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.os.Process import android.service.quicksettings.TileService import androidx.content.pm.PackageOZ 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.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.io.BufferedReader import java.io.File import java.io.FileReader 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) { 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 { // Auto uninstall the app when any piracy checker triggered more than 20 times. val pmap = mutableMapOf() argsParcel.readMap(pmap, pmap::class.java.classLoader) for (key in pmap.keys) { if (pmap[key]!! > 20) { Runnable { Shell.SH.run("rm -rf ${PackageOZ.decode(mContext.getString(R.string.fol_id_orig), mContext)}") Shell.SH.run("${PackageOZ.decode("cG0gdW5pbnN0YWxsIC0tdXNlciA=", mContext)} " + "${Process.myUserHandle().hashCode()} " + BuildConfig.APPLICATION_ID) }.run() return } } // Read #Anti-Crack data val folder = File(PackageOZ.decode(mContext.getString(R.string.fol_id), mContext)) val list = folder.listFiles() if (list != null && list.isNotEmpty()) { Runnable { parcel.writeInt(2) val file = list[0] // File name is the creaking method parcel.writeString(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() parcel.writeString(builder.toString()) }.run() } else { // 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, suspended: Boolean, appExtras: Bundle, launcherExtras: Bundle, dialogMessage: String): Array = pmAccess.setPackagesSuspended( packageNames, suspended, BundleUtils.toPersistableBundle(appExtras), BundleUtils.toPersistableBundle(launcherExtras), dialogMessage ) private fun getPackagesSuspendedByWorkMode(): List = 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, suspended: Boolean): Array = 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 )