diff options
Diffstat (limited to 'app/src/main/java/moe/yuuta/workmode/access')
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 |