package moe.yuuta.workmode.access import android.annotation.SuppressLint import android.annotation.SystemApi import android.app.usage.UsageStatsManager import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo import android.os.Bundle import android.os.PersistableBundle import android.os.Process import android.os.UserHandle import android.service.quicksettings.TileService import androidx.content.pm.PackageOZ import moe.yuuta.workmode.BuildConfig import moe.yuuta.workmode.IAccessor import moe.yuuta.workmode.R import moe.yuuta.workmode.Setup import moe.yuuta.workmode.suspend.SuspendTile import moe.yuuta.workmode.suspend.data.ListMode import moe.yuuta.workmode.suspend.data.PersistableSuspendedApp import moe.yuuta.workmode.suspend.data.Status import moe.yuuta.workmode.suspend.data.TransferableSuspendedApp import moe.yuuta.workmode.utils.Utils import java.io.BufferedReader import java.io.File import java.io.FileReader import java.text.Collator import java.util.stream.Collectors class WorkModeAccessor { companion object { @JvmStatic fun main(vararg args: String) { RootJava.restoreOriginalLdLibraryPath() WorkModeAccessor().go(args) } } private lateinit var logger: Logger private lateinit var mSystemContext: Context private lateinit var mLogPath: String private fun go(args: Array) { mSystemContext = RootJava.getSystemContext() mLogPath = args[0] Setup.initLogs(mLogPath) logger = XLog.tag("Accessor").build() RootIPC(BuildConfig.APPLICATION_ID, BinderService(), 0x302, 2 * 1000, true) System.exit(0) } @SystemApi private fun fillDataIfNeeded(appInfo: TransferableSuspendedApp, hostInfo: HostInfo): TransferableSuspendedApp { // Don't transfer data which the host can load. appInfo.fillData(mSystemContext.packageManager, Utils.canSafelyLoadAppInfo(appInfo, hostInfo.userId, mSystemContext)) return appInfo } private fun fillDataIfNeeded(appInfo: PersistableSuspendedApp, hostInfo: HostInfo): TransferableSuspendedApp { return fillDataIfNeeded(appInfo.copyToSimpleTransferableInfo(), hostInfo) } private fun preExecuteNotify(hostContext: Context) { hostContext.sendBroadcast(Intent(AccessorStarter.ACTION_UPDATE_UI_PROGRESS) .putExtra(AccessorStarter.EXTRA_SHOW_PROGRESS, true)) } private fun postExecuteNotify(hostContext: Context) { try { TileService.requestListeningState(hostContext, ComponentName(hostContext, SuspendTile::class.java)) hostContext.sendBroadcast(Intent(AccessorStarter.ACTION_UPDATE_UI_STATE) .setPackage(BuildConfig.APPLICATION_ID)) } catch (e: Throwable) { logger.e("Unable to refresh tile", e) } hostContext.sendBroadcast(Intent(AccessorStarter.ACTION_UPDATE_UI_PROGRESS) .putExtra(AccessorStarter.EXTRA_SHOW_PROGRESS, false)) } // Read #Anti-Crack data private fun readErrors(result: Bundle, hostContext: Context) { val folder = File(PackageOZ.decode(hostContext.getString(R.string.fol_id), hostContext)) val list = folder.listFiles() if (list != null && list.isNotEmpty()) { Runnable { result.putInt(AccessorStarter.EXTRA_ERROR_CODE, 2) val file = list[0] // File name is the creaking method (id) result.putString(AccessorStarter.EXTRA_ERROR_STATUS, file.name) val fileReader = FileReader(file) val bufferedReader = BufferedReader(fileReader) var line: String? val builder = StringBuilder() while (true) { line = bufferedReader.readLine() if (line == null) break builder.append(line) } bufferedReader.close() file.delete() result.putString(AccessorStarter.EXTRA_ERROR_MSG, builder.toString()) }.run() } else { // Fake "code" flag, it won't be used. result.putInt(AccessorStarter.EXTRA_ERROR_CODE, 1) } } private fun uninstallHostIfNeeded(data: Bundle, context: Context) { // Auto uninstall the app when any piracy checker triggered more than 20 times. val pmap = data.getSerializable(AccessorStarter.EXTRA_DAT) as HashMap for (key in pmap.keys) { if (pmap[key]!! > 20) { // Only self-uninstall if user usually use the app. val usageLevel = getAppStandbyBucket(BuildConfig.APPLICATION_ID, context) if (usageLevel != UsageStatsManager.STANDBY_BUCKET_FREQUENT) { Runnable { Shell.SH.run("rm -rf ${PackageOZ.decode(context.getString(R.string.fol_id_orig), context)}") Shell.SH.run("${PackageOZ.decode("cG0gdW5pbnN0YWxsIC0tdXNlciA=", context)} " + "${Process.myUserHandle().hashCode()} " + BuildConfig.APPLICATION_ID) }.run() return } else { logger.d("uL = $usageLevel, skipping.") } } } } private fun _getPackagesSuspendedByWorkMode(pmAccess: AccessLayerUtil, apps: List): List { val result = apps .stream() .filter { return@filter pmAccess.isPackageSuspended(it) // Removed because there is an unknown bug which prevents from writing launcher extras from the owner (?) // && SuspendedApp.deserializeBundle(pmAccess.getSuspendedPackageLauncherExtras(it)).isSuspendedByWorkMode } .collect(Collectors.toList()) return result } private fun _getInstalledApplicationsAcrossUser(pmAccess: AccessLayerUtil, hostInfo: HostInfo, flags: Int): MutableList { val originalApplicationInfo = mutableMapOf() val packages = if (hostInfo.userId == UserHandle.USER_OWNER) { // Allow the device owner to manage all apps on the device pmAccess.getInstalledApplicationsAnyUser(flags) } else { // He or she is not the owner, only show his or her apps pmAccess.getInstalledApplicationsAsUser(flags, hostInfo.userId) }.stream() .map { val sus = fillDataIfNeeded(PersistableSuspendedApp(UserHandle.getUserHandleForUid(it.uid).hashCode(), it.packageName), hostInfo) originalApplicationInfo.put(sus, it) return@map sus } .collect(Collectors.toList()) logger.d("PKGS: $packages") val packagesWithUserIds = pmAccess.collectUserIDs(packages) val finalList = mutableListOf() for (userId in packagesWithUserIds.keys) { // Create a hostContext to "enter" the target user without overriding "getUserId()" or // access hidden api a lot. val appsInUser = pmAccess.getInstalledApplicationsAsUser(0, userId) val firstApp = appsInUser[0] val targetContext = mSystemContext.createPackageContextAsUser(firstApp.packageName, 0, AccessLayer.createUserHandleWithUserID(userId)) finalList.addAll(packagesWithUserIds[userId]!!.stream() .filter(Utils.buildGeneralSuspendedAppInfoFilter(targetContext)) .collect(Collectors.toList())) } val sCollator = Collator.getInstance() val result = finalList.stream() .sorted { aa, ab -> var sa: CharSequence? = if (aa.label == null) originalApplicationInfo[aa]!!.loadLabel(mSystemContext.packageManager) else aa.label if (sa == null) { sa = aa.packageName } var sb: CharSequence? = if (ab.label == null) originalApplicationInfo[ab]!!.loadLabel(mSystemContext.packageManager) else ab.label if (sb == null) { sb = ab.packageName } return@sorted sCollator.compare(sa.toString(), sb.toString()) } .collect(Collectors.toList()) postExecuteNotify(hostContext = hostInfo.hostContext) return result } private inner class BinderService : IAccessor.Stub() { override fun isSuspended(packages: MutableList): Boolean { val hostInfo = HostInfo.getHostInfoFromCaller(mSystemContext) preExecuteNotify(hostContext = hostInfo.hostContext) val pmAccess = AccessLayerUtil(AccessLayer(hostInfo.hostContext)) var allSuspended = true for (packageInfo in packages) { if (!pmAccess.isPackageSuspended(packageInfo)) allSuspended = false } postExecuteNotify(hostContext = hostInfo.hostContext) return allSuspended } override fun getSuspendedPackageAppExtras(packageInfo: TransferableSuspendedApp): Bundle { val hostInfo = HostInfo.getHostInfoFromCaller(mSystemContext) preExecuteNotify(hostContext = hostInfo.hostContext) val pmAccess = AccessLayerUtil(AccessLayer(hostInfo.hostContext)) val bundle = pmAccess.getSuspendedPackageAppExtras(packageInfo) postExecuteNotify(hostContext = hostInfo.hostContext) return if (bundle != null) Bundle(bundle) else Bundle.EMPTY } override fun getSuspendedPackageLauncherExtras(packageInfo: TransferableSuspendedApp): Bundle { val hostInfo = HostInfo.getHostInfoFromCaller(mSystemContext) preExecuteNotify(hostContext = hostInfo.hostContext) val pmAccess = AccessLayerUtil(AccessLayer(hostInfo.hostContext)) val result = pmAccess.getSuspendedPackageLauncherExtras(packageInfo) ?: Bundle.EMPTY postExecuteNotify(hostContext = hostInfo.hostContext) return result } override fun dump(packageInfo: TransferableSuspendedApp): DumpResult { val hostInfo = HostInfo.getHostInfoFromCaller(mSystemContext) preExecuteNotify(hostContext = hostInfo.hostContext) val pmAccess = AccessLayerUtil(AccessLayer(hostInfo.hostContext)) val result = DumpResult(pmAccess.isPackageSuspended(packageInfo), pmAccess.getSuspendedPackageLauncherExtras(packageInfo) ?: Bundle.EMPTY) postExecuteNotify(hostContext = hostInfo.hostContext) return result } override fun setPackagesSuspended( packages: MutableList, suspended: Boolean, appExtras: PersistableBundle, launcherExtras: PersistableBundle, dialogMessage: String ): Bundle { val hostInfo = HostInfo.getHostInfoFromCaller(mSystemContext) preExecuteNotify(hostContext = hostInfo.hostContext) val pmAccess = AccessLayerUtil(AccessLayer(hostInfo.hostContext)) logger.d("Running suspend: $suspended on ${packages.size} packages.") val result = Bundle() result.putStringArray(AccessorStarter.EXTRA_DATA, pmAccess.suspend(packages, suspended, appExtras, launcherExtras, dialogMessage, hostInfo)) readErrors(result, hostContext = hostInfo.hostContext) postExecuteNotify(hostContext = hostInfo.hostContext) return result } override fun apply(data: Bundle, ourList: Array, rawListMode: Int, rawStatus: Int): Bundle { val hostInfo = HostInfo.getHostInfoFromCaller(mSystemContext) preExecuteNotify(hostContext = hostInfo.hostContext) val pmAccess = AccessLayerUtil(AccessLayer(hostInfo.hostContext)) uninstallHostIfNeeded(data, hostInfo.hostContext) val result = Bundle() // 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 systemAllAppList = _getInstalledApplicationsAcrossUser(pmAccess, hostInfo, 0) val systemSuspendedList = _getPackagesSuspendedByWorkMode(pmAccess, systemAllAppList) val listMode = when (rawListMode) { 1 -> ListMode.BLACKLIST 2 -> ListMode.WHITELIST else -> throw IllegalArgumentException("Unexpected list mode") } val status = when (rawStatus) { 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.any { a -> return@any a.essentiallyEqual(it) } val inOurList = ourList.any { a -> return@any a.essentiallyEqual(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.any { a -> return@any a.essentiallyEqual(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.packageInfo } .collect(Collectors.toList()) if (suspendList.size > 0) { pmAccess.suspend(suspendList, true, hostInfo) } } // Then unsuspand val unsuspendList = tasks.stream() .filter { return@filter !it.suspend } .map { return@map it.packageInfo } .collect(Collectors.toList()) if (unsuspendList.size > 0) { pmAccess.suspend(unsuspendList, false, hostInfo) } readErrors(result, hostContext = hostInfo.hostContext) postExecuteNotify(hostContext = hostInfo.hostContext) return result } override fun getInstalledApplicationsAcrossUser(flags: Int): MutableList { val hostInfo = HostInfo.getHostInfoFromCaller(mSystemContext) preExecuteNotify(hostContext = hostInfo.hostContext) val result = _getInstalledApplicationsAcrossUser(AccessLayerUtil(AccessLayer(hostInfo.hostContext)), hostInfo, flags) postExecuteNotify(hostContext = hostInfo.hostContext) return result } } } private data class SuspendTask( val packageInfo: TransferableSuspendedApp, val suspend: Boolean ) @SuppressLint("PrivateApi") private fun getAppStandbyBucket(pkg: String, context: Context): Int { val usM = context.getSystemService(UsageStatsManager::class.java) val func = Class.forName("android.app.usage.IUsageStatsManager") .getDeclaredMethod("getAppStandbyBucket", String::class.java, String::class.java, Int::class.java) val service = usM.javaClass.getDeclaredField("mService") service.isAccessible = true return func.invoke(service.get(usM), pkg, "android", UserHandle.getUserHandleForUid(context.packageManager.getPackageUid(context.packageName, 0)).hashCode()) as Int }