aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/moe/yuuta/workmode/access
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/moe/yuuta/workmode/access')
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/AccessLayer.kt86
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/AccessorStarter.kt199
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/ApplicationAccessorStarter.kt9
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/DumpResult.kt7
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/ShellAccessorStarter.kt9
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/WorkModeAccessor.kt317
6 files changed, 627 insertions, 0 deletions
diff --git a/app/src/main/java/moe/yuuta/workmode/access/AccessLayer.kt b/app/src/main/java/moe/yuuta/workmode/access/AccessLayer.kt
new file mode 100644
index 0000000..78ef5dd
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/access/AccessLayer.kt
@@ -0,0 +1,86 @@
+package moe.yuuta.workmode.access
+
+import android.content.Context
+import android.content.pm.LauncherApps
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.os.PersistableBundle
+import android.os.Process
+import android.os.UserHandle
+import android.system.Os
+import moe.yuuta.workmode.BuildConfig
+import java.lang.reflect.Field
+import java.lang.reflect.Method
+
+/**
+ * An layer to access package suspending related APIs, it is a low-level layer which is used to call System APIs directly.
+ *
+ * TODO: Multi-user support
+ */
+internal class AccessLayer(private val mContext: Context) {
+ private val mPM: PackageManager = mContext.packageManager
+
+ fun setPackagesSuspended(packageNames: Array<String>, suspended: Boolean,
+ appExtras: PersistableBundle, launcherExtras: PersistableBundle,
+ dialogMessage: String): Array<String> {
+ // ApplicationPackageManager ALWAYS uses context.getOpPackageName() as the argument "callingPackage"
+ // My callingPackage MUSTN'T equals to 'android'
+ // If we are using packageName of 'android', system will show disabled
+ // by admin dialog instead of suspended dialog
+ // F**k Google
+ val func: Method = Class.forName("android.content.pm.IPackageManager")
+ .getDeclaredMethod("setPackagesSuspendedAsUser",
+ Array<String>::class.java,
+ Boolean::class.java,
+ PersistableBundle::class.java,
+ PersistableBundle::class.java,
+ String::class.java,
+ String::class.java,
+ Int::class.java)
+
+ // It's an unstable design
+ val iPM: Field = mPM::class.java.getDeclaredField("mPM")
+ iPM.isAccessible = true
+
+ return func.invoke(iPM.get(mPM),
+ packageNames,
+ suspended,
+ appExtras,
+ launcherExtras,
+ dialogMessage,
+ BuildConfig.APPLICATION_ID,
+ UserHandle.getUserHandleForUid(mPM.getPackageUid(mContext.packageName, 0)).hashCode()) as Array<String>
+ }
+
+ /**
+ * This method will SET your UID and you WON'T BE ABLE TO GO BACK.
+ * Create a new process and access it.
+ */
+ fun getSuspendedPackageAppExtras(packageName: String): PersistableBundle? {
+ Os.setuid(mPM.getPackageUid(packageName, 0))
+ // ApplicationPackageManager ALWAYS uses context.getOpPackageName() as the package name
+ // F**k Google
+ val func: Method = Class.forName("android.content.pm.IPackageManager")
+ .getDeclaredMethod("getSuspendedPackageAppExtras",
+ String::class.java,
+ Int::class.java)
+
+ // It's an unstable design
+ val iPM: Field = mPM::class.java.getDeclaredField("mPM")
+ iPM.isAccessible = true
+
+ return func.invoke(iPM.get(mPM),
+ packageName,
+ UserHandle.getUserHandleForUid(mPM.getPackageUid(packageName, 0)).hashCode()) as PersistableBundle?
+ }
+
+ @Throws(PackageManager.NameNotFoundException::class)
+ fun isPackageSuspended(packageName: String): Boolean {
+ val func: Method = PackageManager::class.java.getDeclaredMethod("isPackageSuspended",
+ String::class.java)
+ return func.invoke(mPM, packageName) as Boolean
+ }
+
+ fun getSuspendedPackageLauncherExtras(packageName: String): Bundle? =
+ mContext.getSystemService(LauncherApps::class.java).getSuspendedPackageLauncherExtras(packageName, Process.myUserHandle())
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/access/AccessorStarter.kt b/app/src/main/java/moe/yuuta/workmode/access/AccessorStarter.kt
new file mode 100644
index 0000000..c8aae67
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/access/AccessorStarter.kt
@@ -0,0 +1,199 @@
+package moe.yuuta.workmode.access
+
+import android.content.Context
+import android.os.Bundle
+import android.os.Parcel
+import android.os.PersistableBundle
+import com.elvishew.xlog.Logger
+import com.elvishew.xlog.XLog
+import eu.chainfire.librootjava.RootJava
+import eu.chainfire.libsuperuser.Shell
+import moe.yuuta.workmode.BuildConfig
+import moe.yuuta.workmode.suspend.data.ListMode
+import moe.yuuta.workmode.suspend.data.Status
+import moe.yuuta.workmode.utils.ByteArraySerializer
+
+/**
+ * The high-level API accessor, as known as the launcher (starter) of the accessor, wraps
+ * the necessary start steps to launch it and deserialize the result.
+ */
+open class AccessorStarter(private val mContext: Context, private val mLogPath: String) {
+ private val logger: Logger = XLog.tag("AccessorStarter").build()
+
+ companion object {
+ const val ACTION_UPDATE_UI_STATE = "moe.yuuta.workmode.access.ACTION_UPDATE_UI_STATE"
+ const val ACTION_UPDATE_UI_PROGRESS = "moe.yuuta.workmode.access.ACTION_UPDATE_UI_PROGRESS"
+ const val EXTRA_SHOW_PROGRESS = "moe.yuuta.workmode.access.EXTRA_SHOW_PROGRESS"
+ }
+
+ private fun launchRootProcess(root: Boolean, vararg args: String): MutableList<String> {
+ val command = RootJava.getLaunchScript(mContext,
+ WorkModeAccessor::class.java,
+ null,
+ null,
+ args,
+ BuildConfig.APPLICATION_ID + ":accessor")
+
+ return if (root) {
+ Shell.SU.run(command)
+ } else {
+ Shell.SH.run(command)
+ }
+ }
+
+ fun getSuspendedPackageAppExtras(packageName: String, root: Boolean): Bundle? {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_GET_APP_EXTRAS)
+ argumentParcel.writeString(packageName)
+ val marshalledResult = launchRootProcess(root,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ val bundle = result.readBundle()
+ result.recycle()
+ return bundle
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+
+ fun getSuspendedPackageLauncherExtras(packageName: String, root: Boolean): Bundle? {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_GET_LAUNCHER_EXTRAS)
+ argumentParcel.writeString(packageName)
+ val marshalledResult = launchRootProcess(root,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ val bundle = result.readBundle()
+ result.recycle()
+ return bundle
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+
+ fun isPackageSuspended(packageNames: Array<out String>, root: Boolean): Boolean {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_IS_SUSPENDED)
+ argumentParcel.writeStringArray(packageNames)
+ val marshalledResult = launchRootProcess(root,
+ WorkModeAccessor.ACTION_IS_SUSPENDED,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ val isSuspended = result.readByte() == 1.toByte()
+ result.recycle()
+ return isSuspended
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+
+ fun dump(packageName: String, root: Boolean): DumpResult {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_DUMP)
+ argumentParcel.writeString(packageName)
+ val marshalledResult = launchRootProcess(root,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ val data = DumpResult(
+ result.readByte() == 1.toByte(),
+ result.readBundle(),
+ result.readBundle()
+ )
+ result.recycle()
+ return data
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+
+ fun setPackagesSuspended(packageNames: Array<String>, suspended: Boolean,
+ appExtras: PersistableBundle, launcherExtras: PersistableBundle,
+ dialogMessage: String, root: Boolean): Array<String> {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_SET_SUSPENDED)
+ argumentParcel.writeStringArray(packageNames)
+ argumentParcel.writeByte(if (suspended) 1 else 0)
+ argumentParcel.writeBundle(Bundle(appExtras))
+ argumentParcel.writeBundle(Bundle(launcherExtras))
+ argumentParcel.writeString(dialogMessage)
+ val marshalledResult = launchRootProcess(root,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ val rs = result.createStringArray() ?: arrayOf()
+ result.recycle()
+ return rs
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+
+ @Throws(Throwable::class)
+ private fun deserialize(byteArray: ByteArray): Parcel {
+ val result = Parcel.obtain()
+ result.unmarshall(byteArray, 0, byteArray.size)
+ // Thanks to https://github.com/jiaminghan/droidplanner-master/blob/743b5436df6311cbbbfdecd21f796e2b948cbac7/Android/src/com/o3dr/services/android/lib/util/ParcelableUtils.java#L35
+ result.setDataPosition(0)
+ when (result.readByte()) {
+ 1.toByte() -> {
+ }
+ 0.toByte() -> {
+ val throwable = result.readSerializable() as Throwable?
+ if (throwable != null) {
+ throw throwable
+ }
+ throw RuntimeException("Unsuccessful result with unknown stacktrace")
+ }
+ }
+ return result
+ }
+
+ fun getPackagesSuspendedByWorkMode(root: Boolean): List<String> {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_GET_ALL_PACKAGES_SUSPENDED_BY_WORK_MODE)
+ val marshalledResult = launchRootProcess(root,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ val data = result.createStringArrayList()
+ result.recycle()
+ return data ?: listOf()
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+
+ fun apply(suspendList: Array<String>, listMode: ListMode, status: Status, root: Boolean) {
+ val argumentParcel: Parcel = Parcel.obtain()
+ try {
+ argumentParcel.writeString(mLogPath)
+ argumentParcel.writeString(WorkModeAccessor.ACTION_APPLY)
+ argumentParcel.writeStringArray(suspendList)
+ argumentParcel.writeInt(when (listMode) {
+ ListMode.BLACKLIST -> 1
+ ListMode.WHITELIST -> 2
+ })
+ argumentParcel.writeInt(when (status) {
+ Status.ON -> 1
+ Status.OFF -> 2
+ })
+ val marshalledResult = launchRootProcess(root,
+ ByteArraySerializer.serialize(argumentParcel.marshall()))[0]
+ val result = deserialize(ByteArraySerializer.deserialize(marshalledResult))
+ result.recycle()
+ } finally {
+ argumentParcel.recycle()
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/access/ApplicationAccessorStarter.kt b/app/src/main/java/moe/yuuta/workmode/access/ApplicationAccessorStarter.kt
new file mode 100644
index 0000000..8715989
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/access/ApplicationAccessorStarter.kt
@@ -0,0 +1,9 @@
+package moe.yuuta.workmode.access
+
+import android.content.Context
+import moe.yuuta.workmode.Setup
+
+/**
+ * A flavor of AccessorStarter which can be used in standard Android Applications
+ */
+class ApplicationAccessorStarter(private val mContext: Context) : AccessorStarter(mContext, Setup.getLogsPath(mContext).absolutePath) \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/access/DumpResult.kt b/app/src/main/java/moe/yuuta/workmode/access/DumpResult.kt
new file mode 100644
index 0000000..d4a9d79
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/access/DumpResult.kt
@@ -0,0 +1,7 @@
+package moe.yuuta.workmode.access
+
+import android.os.Bundle
+
+data class DumpResult(val isSuspended: Boolean,
+ val appExtras: Bundle?,
+ val launcherExtras: Bundle?) \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/access/ShellAccessorStarter.kt b/app/src/main/java/moe/yuuta/workmode/access/ShellAccessorStarter.kt
new file mode 100644
index 0000000..05c4983
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/access/ShellAccessorStarter.kt
@@ -0,0 +1,9 @@
+package moe.yuuta.workmode.access
+
+import eu.chainfire.librootjava.RootJava
+import moe.yuuta.workmode.BuildConfig
+
+/**
+ * A flavor of AccessorStarter which can be used in shell programs
+ */
+class ShellAccessorStarter(private val mLogPath: String) : AccessorStarter(RootJava.getPackageContext(BuildConfig.APPLICATION_ID), mLogPath) \ No newline at end of file
diff --git a/app/src/main/java/moe/yuuta/workmode/access/WorkModeAccessor.kt b/app/src/main/java/moe/yuuta/workmode/access/WorkModeAccessor.kt
new file mode 100644
index 0000000..d8f17cd
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/access/WorkModeAccessor.kt
@@ -0,0 +1,317 @@
+package moe.yuuta.workmode.access
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Parcel
+import android.os.PersistableBundle
+import android.service.quicksettings.TileService
+import com.elvishew.xlog.Logger
+import com.elvishew.xlog.XLog
+import eu.chainfire.librootjava.RootJava
+import moe.yuuta.workmode.BuildConfig
+import moe.yuuta.workmode.R
+import moe.yuuta.workmode.Setup
+import moe.yuuta.workmode.suspend.SuspendTile
+import moe.yuuta.workmode.suspend.SuspendedApp
+import moe.yuuta.workmode.suspend.data.ListMode
+import moe.yuuta.workmode.suspend.data.Status
+import moe.yuuta.workmode.utils.BundleUtils
+import moe.yuuta.workmode.utils.ByteArraySerializer
+import moe.yuuta.workmode.utils.Utils
+import java.util.stream.Collectors
+
+class WorkModeAccessor {
+ companion object {
+ const val ACTION_GET_APP_EXTRAS = "get_app_extras"
+ const val ACTION_IS_SUSPENDED = "is_suspended"
+ const val ACTION_GET_LAUNCHER_EXTRAS = "get_launcher_extras"
+ const val ACTION_SET_SUSPENDED = "set_suspended"
+ const val ACTION_DUMP = "dump"
+ const val ACTION_GET_ALL_PACKAGES_SUSPENDED_BY_WORK_MODE = "get_all_packages_suspended_by_work_mode"
+ const val ACTION_APPLY = "apply"
+
+ @JvmStatic
+ fun main(vararg args: String) {
+ RootJava.restoreOriginalLdLibraryPath()
+ WorkModeAccessor().go(args)
+ }
+ }
+
+ private lateinit var logger: Logger
+ private lateinit var mContext: Context
+ private lateinit var pmAccess: AccessLayer
+ private lateinit var mLogPath: String
+
+ private fun go(args: Array<out String>) {
+ mContext = RootJava.getPackageContext(BuildConfig.APPLICATION_ID)
+ pmAccess = AccessLayer(mContext)
+ mContext.sendBroadcast(Intent(AccessorStarter.ACTION_UPDATE_UI_PROGRESS)
+ .putExtra(AccessorStarter.EXTRA_SHOW_PROGRESS, true))
+ var parcel = Parcel.obtain()
+ val argsByteArray = ByteArraySerializer.deserialize(args[0])
+ val argsParcel = Parcel.obtain()
+ argsParcel.unmarshall(argsByteArray, 0, argsByteArray.size)
+ argsParcel.setDataPosition(0)
+ mLogPath = argsParcel.readString() ?: "/data/adb"
+ Setup.initLogs(mLogPath)
+ logger = XLog.tag("Accessor").build()
+ try {
+ // General successful flag: 1 = success; 0 = unsuccessful
+ parcel.writeByte(1)
+ runGo(argsParcel, parcel)
+ } catch (e: Throwable) {
+ logger.e("Unexpected exception caused in accessor", e)
+ parcel.recycle()
+ // Re-mark it as unsuccessful
+ parcel = Parcel.obtain()
+ parcel.writeByte(0)
+ parcel.writeSerializable(e)
+ }
+ try {
+ TileService.requestListeningState(mContext, ComponentName(mContext, SuspendTile::class.java))
+ mContext.sendBroadcast(Intent(AccessorStarter.ACTION_UPDATE_UI_STATE)
+ .setPackage(BuildConfig.APPLICATION_ID))
+ } catch (e: Throwable) {
+ logger.e("Unable to refresh tile", e)
+ }
+ System.out.println(ByteArraySerializer.serialize(parcel.marshall()))
+ parcel.recycle()
+ mContext.sendBroadcast(Intent(AccessorStarter.ACTION_UPDATE_UI_PROGRESS)
+ .putExtra(AccessorStarter.EXTRA_SHOW_PROGRESS, false))
+ System.exit(0)
+ }
+
+ private fun runGo(argsParcel: Parcel, parcel: Parcel) {
+ when(argsParcel.readString()) {
+ ACTION_GET_APP_EXTRAS -> {
+ val bundle = pmAccess.getSuspendedPackageAppExtras(argsParcel.readString() ?: "android")
+ parcel.writeBundle(if (bundle != null) Bundle(bundle) else Bundle.EMPTY)
+ }
+ ACTION_IS_SUSPENDED -> {
+ val packageNames = argsParcel.createStringArray() ?: arrayOf("android")
+ var allSuspended = true
+ for (packageName in packageNames) {
+ if (!pmAccess.isPackageSuspended(packageName)) allSuspended = false
+ }
+ parcel.writeByte(if (allSuspended) 1 else 0)
+ }
+ ACTION_GET_LAUNCHER_EXTRAS -> {
+ parcel.writeBundle(pmAccess.getSuspendedPackageLauncherExtras(argsParcel.readString() ?: "android") ?: Bundle.EMPTY)
+ }
+ ACTION_SET_SUSPENDED -> {
+ val packageNames = argsParcel.createStringArray() ?: arrayOf("android")
+ val suspended = argsParcel.readByte() == 1.toByte()
+ logger.d("Running suspend: $suspended on ${packageNames.size} packages.")
+ val appExtras = argsParcel.readBundle()
+ val launcherExtras = argsParcel.readBundle()
+ val dialogMessage = argsParcel.readString() ?: "WorkMode"
+ argsParcel.recycle()
+ parcel.writeStringArray(suspend(packageNames, suspended, appExtras, launcherExtras, dialogMessage))
+ }
+ ACTION_DUMP -> {
+ val pkg = argsParcel.readString() ?: "android"
+ parcel.writeByte(if (pmAccess.isPackageSuspended(pkg)) 1 else 0)
+ parcel.writeBundle(ShellAccessorStarter(mLogPath).getSuspendedPackageAppExtras(pkg, false))
+ parcel.writeBundle(pmAccess.getSuspendedPackageLauncherExtras(pkg) ?: Bundle.EMPTY)
+ }
+ ACTION_GET_ALL_PACKAGES_SUSPENDED_BY_WORK_MODE -> {
+ parcel.writeStringList(getPackagesSuspendedByWorkMode())
+ }
+ ACTION_APPLY -> {
+ apply(argsParcel)
+ }
+ }
+ }
+
+ private fun suspend(packageNames: Array<String>, suspended: Boolean,
+ appExtras: Bundle, launcherExtras: Bundle,
+ dialogMessage: String): Array<String> =
+ pmAccess.setPackagesSuspended(
+ packageNames,
+ suspended,
+ BundleUtils.toPersistableBundle(appExtras),
+ BundleUtils.toPersistableBundle(launcherExtras),
+ dialogMessage
+ )
+
+ private fun getPackagesSuspendedByWorkMode(): List<String> =
+ mContext.packageManager.getInstalledApplications(0)
+ .stream()
+ .filter(Utils.buildGeneralApplicationInfoFilter(mContext))
+ .filter {
+ return@filter pmAccess.isPackageSuspended(it.packageName) &&
+ SuspendedApp.deserializeBundle(pmAccess.getSuspendedPackageLauncherExtras(it.packageName)).isSuspendedByWorkMode
+ }
+ .map {
+ return@map it.packageName
+ }
+ .collect(Collectors.toList())
+
+ private fun apply(args: Parcel) {
+ // Compare system's list and ours.
+ // Blacklist:
+ // System suspended -> {
+ // in our list -> ON - don't care; OFF - unsuspend
+ // not in our list -> unsuspend
+ // }
+ // System not suspended -> {
+ // in our list -> ON - suspend; OFF - don't care
+ // not in our list -> don't care
+ // }
+ // Whitelist:
+ // System suspended -> {
+ // in our whitelist -> unsuspend
+ // not in our whitelist -> ON - don't care; OFF - unsuspend
+ // }
+ // System not suspended -> {
+ // in our whitelist -> don't care
+ // not in our whitelist -> ON - suspend; OFF - don't care
+ // }
+
+ // This is the plan for Off->On or On->On situations. If we are heading
+ // Off, we just ignore all tasks which is going to suspend an app. Because
+ // we need to restore.
+
+ // We use these two lists to determine whatever an app is suspended
+ // It it is suspended but not appears in systemSuspendedList, we know that
+ // it is suspended by other apps, like D**ital Wellbeing, we can just override it.
+ val systemSuspendedList = getPackagesSuspendedByWorkMode()
+ val systemAllAppList = mContext.packageManager.getInstalledApplications(0)
+ .stream()
+ .filter(Utils.buildGeneralApplicationInfoFilter(mContext))
+ .map { return@map it.packageName }
+ .collect(Collectors.toList())
+ val ourList = args.createStringArrayList()
+ val listMode = when (args.readInt()) {
+ 1 -> ListMode.BLACKLIST
+ 2 -> ListMode.WHITELIST
+ else -> throw IllegalArgumentException("Unexpected list mode")
+ }
+ val status = when (args.readInt()) {
+ 1 -> Status.ON
+ 2 -> Status.OFF
+ else -> throw IllegalArgumentException("Unexpected status")
+ }
+
+ val tasks = systemAllAppList.stream()
+ // Filter "don't care" situations, do not map them here.
+ .filter {
+ val systemSuspended = systemSuspendedList.contains(it)
+ val inOurList = ourList.contains(it)
+ when (listMode) {
+ ListMode.BLACKLIST -> {
+ if (systemSuspended) {
+ if (inOurList) {
+ if (status == Status.ON) {
+ return@filter false
+ } else {
+ return@filter true
+ }
+ } else {
+ if (status == Status.ON) {
+ return@filter true
+ } else {
+ return@filter false
+ }
+ }
+ } else {
+ if (status == Status.ON) {
+ return@filter inOurList
+ } else {
+ return@filter false
+ }
+ }
+ }
+ ListMode.WHITELIST -> {
+ if (systemSuspended) {
+ if (inOurList) {
+ return@filter true
+ } else {
+ if (status == Status.ON) {
+ return@filter false
+ } else {
+ return@filter true
+ }
+ }
+ } else {
+ if (status == Status.ON) {
+ return@filter !inOurList
+ } else {
+ return@filter false
+ }
+ }
+ }
+ }
+ }
+ // Now, map them and determine that whatever a package should be suspended or un-suspended.
+ .map {
+ val systemSuspended = systemSuspendedList.contains(it)
+ when (listMode) {
+ ListMode.BLACKLIST -> {
+ if (systemSuspended) {
+ // It must be off (if in our list),
+ // or need to un-suspend (whatever on or off)
+ return@map SuspendTask(it, false)
+ } else {
+ // It must be on
+ return@map SuspendTask(it, true)
+ }
+ }
+ ListMode.WHITELIST -> {
+ if (systemSuspended) {
+ // It must be un-suspended (if in our list)
+ // (or off)
+ return@map SuspendTask(it, false)
+ } else {
+ // It must be on
+ return@map SuspendTask(it, true)
+ }
+ }
+ }
+ }
+ // Collect them, we will execute later.
+ .collect(Collectors.toList())
+ // Suspend first
+ if (status == Status.ON) {
+ val suspendList = tasks.stream()
+ .filter {
+ return@filter it.suspend
+ }
+ .map {
+ return@map it.packageName
+ }
+ .collect(Collectors.toList())
+ if (suspendList.size > 0) {
+ suspend(suspendList.toTypedArray(),
+ true)
+ }
+ }
+ // Then unsuspand
+ val unsuspendList = tasks.stream()
+ .filter {
+ return@filter !it.suspend
+ }
+ .map {
+ return@map it.packageName
+ }
+ .collect(Collectors.toList())
+ if (unsuspendList.size > 0) {
+ suspend(unsuspendList.toTypedArray(),
+ false)
+ }
+ }
+
+ private fun suspend(packageNames: Array<String>, suspended: Boolean): Array<String> =
+ pmAccess.setPackagesSuspended(packageNames,
+ suspended,
+ PersistableBundle(),
+ SuspendedApp.getDefault().serializeBundle(), // We use LauncherExtras because they are easy to read
+ mContext.getString(R.string.suspended_message))
+}
+
+private data class SuspendTask(
+ val packageName: String,
+ val suspend: Boolean
+) \ No newline at end of file