From 06fbdcac173aea88cb4d02c4806866c83e720307 Mon Sep 17 00:00:00 2001 From: YuutaW <17158086+trumeet@users.noreply.github.com> Date: Sat, 30 Mar 2019 16:19:07 -0700 Subject: feat(app/ci): implement multi user support Signed-off-by: YuutaW <17158086+Trumeet@users.noreply.github.com> --- .travis.yml | 3 + app/src/debug/AndroidManifest.xml | 12 + .../java/moe/yuuta/workmode/debug/DebugActivity.kt | 139 ++++++ app/src/main/AndroidManifest.xml | 4 + .../main/aidl/moe/yuuta/workmode/IAccessor.aidl | 19 + .../aidl/moe/yuuta/workmode/access/DumpResult.aidl | 4 + .../suspend/data/TransferableSuspendedApp.aidl | 4 + .../java/moe/yuuta/ext/HandlerThreadExecutor.java | 26 ++ .../java/moe/yuuta/gplicense/LicenseChecker.java | 22 +- .../moe/yuuta/gplicense/ServerManagedPolicy.java | 6 +- .../yuuta/workmode/ApplicationPickerActivity.kt | 139 +++--- .../main/java/moe/yuuta/workmode/MainActivity.kt | 116 +++-- .../java/moe/yuuta/workmode/access/AccessLayer.kt | 63 +-- .../moe/yuuta/workmode/access/AccessLayerUtil.kt | 102 +++++ .../moe/yuuta/workmode/access/AccessorStarter.kt | 297 ++++++------ .../workmode/access/ApplicationAccessorStarter.kt | 9 - .../java/moe/yuuta/workmode/access/DumpResult.kt | 29 +- .../java/moe/yuuta/workmode/access/HostInfo.kt | 25 + .../yuuta/workmode/access/ShellAccessorStarter.kt | 9 - .../moe/yuuta/workmode/access/WorkModeAccessor.kt | 510 ++++++++++++--------- .../moe/yuuta/workmode/suspend/AsyncSuspender.kt | 60 ++- .../java/moe/yuuta/workmode/suspend/SuspendTile.kt | 10 +- .../moe/yuuta/workmode/suspend/SuspendedApp.kt | 38 -- .../java/moe/yuuta/workmode/suspend/Suspender.kt | 40 -- .../suspend/data/PersistableSuspendedApp.kt | 41 ++ .../workmode/suspend/data/SuspendedStorage.kt | 38 +- .../suspend/data/TransferableSuspendedApp.kt | 101 ++++ .../main/java/moe/yuuta/workmode/utils/Utils.kt | 104 +++++ app/src/main/res/layout/item_application.xml | 20 +- .../main/res/layout/item_application_select.xml | 20 +- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 32 files changed, 1346 insertions(+), 666 deletions(-) create mode 100644 app/src/debug/AndroidManifest.xml create mode 100644 app/src/debug/java/moe/yuuta/workmode/debug/DebugActivity.kt create mode 100644 app/src/main/aidl/moe/yuuta/workmode/IAccessor.aidl create mode 100644 app/src/main/aidl/moe/yuuta/workmode/access/DumpResult.aidl create mode 100644 app/src/main/aidl/moe/yuuta/workmode/suspend/data/TransferableSuspendedApp.aidl create mode 100644 app/src/main/java/moe/yuuta/ext/HandlerThreadExecutor.java create mode 100644 app/src/main/java/moe/yuuta/workmode/access/AccessLayerUtil.kt delete mode 100644 app/src/main/java/moe/yuuta/workmode/access/ApplicationAccessorStarter.kt create mode 100644 app/src/main/java/moe/yuuta/workmode/access/HostInfo.kt delete mode 100644 app/src/main/java/moe/yuuta/workmode/access/ShellAccessorStarter.kt delete mode 100644 app/src/main/java/moe/yuuta/workmode/suspend/SuspendedApp.kt delete mode 100644 app/src/main/java/moe/yuuta/workmode/suspend/Suspender.kt create mode 100644 app/src/main/java/moe/yuuta/workmode/suspend/data/PersistableSuspendedApp.kt create mode 100644 app/src/main/java/moe/yuuta/workmode/suspend/data/TransferableSuspendedApp.kt diff --git a/.travis.yml b/.travis.yml index 2e9ae07..83e948e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,9 @@ script: - "./gradlew :app:assembleRelease --daemon --parallel" before_install: - yes | sdkmanager "platforms;android-28" +- wget https://github.com/anggrayudi/android-hidden-api/files/2709802/android.zip +- rm ~/Android/Sdk/platforms/android-28/android.jar +- mv ./android.zip ~/Android/Sdk/platforms/android-28/android.jar - chmod a+x gradlew - openssl aes-256-cbc -K $encrypted_8cd0a575b07e_key -iv $encrypted_8cd0a575b07e_iv -in secrets.tar.enc -out ./secrets.tar -d diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..92efdb0 --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/debug/java/moe/yuuta/workmode/debug/DebugActivity.kt b/app/src/debug/java/moe/yuuta/workmode/debug/DebugActivity.kt new file mode 100644 index 0000000..b8b5af8 --- /dev/null +++ b/app/src/debug/java/moe/yuuta/workmode/debug/DebugActivity.kt @@ -0,0 +1,139 @@ +package moe.yuuta.workmode.debug + +import android.graphics.Typeface +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.method.ScrollingMovementMethod +import android.util.Log +import android.view.ViewGroup +import android.widget.Button +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import moe.yuuta.ext.HandlerThreadExecutor +import moe.yuuta.workmode.access.DumpResult +import moe.yuuta.workmode.suspend.AsyncSuspender +import moe.yuuta.workmode.suspend.data.TransferableSuspendedApp +import moe.yuuta.workmode.utils.Utils +import java.util.concurrent.CompletableFuture + +class DebugActivity : AppCompatActivity() { + companion object { + private const val TAG = "Debug" + } + + private var mCurrentFuture: CompletableFuture<*>? = null + private lateinit var mLogText: TextView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Log.i(TAG, "Starting") + val rootView = LinearLayout(this) + rootView.orientation = LinearLayout.VERTICAL + + mLogText = TextView(this) + mLogText.typeface = Typeface.MONOSPACE + mLogText.movementMethod = ScrollingMovementMethod() + rootView.addView(mLogText) + + registerTestingFunction(rootView, "isSuspended", object : Callback { + override fun call(): CompletableFuture<*> { + return AsyncSuspender(this@DebugActivity).isSuspended(listOf(TransferableSuspendedApp.of("kh.android.dir"), + TransferableSuspendedApp.of("com.android.chrome"))) + } + }) + + registerTestingFunction(rootView, "getSuspendedPackageAppExtras", object : Callback { + override fun call(): CompletableFuture<*> { + return AsyncSuspender(this@DebugActivity).getSuspendedPackageAppExtras(TransferableSuspendedApp.of("com.android.chrome")) + } + }) + + registerTestingFunction(rootView, "getSuspendedPackageLauncherExtras", object : Callback { + override fun call(): CompletableFuture<*> { + return AsyncSuspender(this@DebugActivity).getSuspendedPackageLauncherExtras(TransferableSuspendedApp.of("com.android.chrome")) + } + }) + + registerTestingFunction(rootView, "dump", object : Callback { + override fun call(): CompletableFuture<*> { + return AsyncSuspender(this@DebugActivity).dump(TransferableSuspendedApp.of("com.google.android.dialer", 11)) + } + }) + + registerTestingFunction(rootView, "suspend (true)", object : Callback { + override fun call(): CompletableFuture<*> { + return AsyncSuspender(this@DebugActivity).suspend(listOf(TransferableSuspendedApp.of("kh.android.dir"), + TransferableSuspendedApp.of("com.android.chrome")), true) + } + }) + + registerTestingFunction(rootView, "suspend (false)", object : Callback { + override fun call(): CompletableFuture<*> { + return AsyncSuspender(this@DebugActivity).suspend(listOf(TransferableSuspendedApp.of("kh.android.dir"), + TransferableSuspendedApp.of("com.android.chrome")), false) + } + }) + + registerTestingFunction(rootView, "apply", object : Callback { + override fun call(): CompletableFuture<*> { + return AsyncSuspender(this@DebugActivity).applyFromSettings() + } + }) + + registerTestingFunction(rootView, "getInstalledApplicationsAcrossUser", object : Callback { + override fun call(): CompletableFuture<*> { + return AsyncSuspender(this@DebugActivity).getInstalledApplicationsAcrossUser(0) + } + }) + + Log.d(TAG, "Done, children amount: ${rootView.childCount}") + setContentView(rootView) + } + + private fun registerTestingFunction(rootView: ViewGroup, tag: String, onClickListener: Callback) { + val btn = Button(this) + btn.text = tag + btn.setOnClickListener { + if (mCurrentFuture != null && !(mCurrentFuture as CompletableFuture<*>).isDone) { + Toast.makeText(this, "Current running task is not done yet.", Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + mLogText.text = "" + mLogText.append("Started task.\n") + mCurrentFuture = onClickListener + .call() + /* .exceptionally { + runOnUiThread { + Log.e(TAG, "Exceptionally ${Thread.currentThread().name}", it) + mLogText.append("\n") + mLogText.append("Exceptionally: $it\n") + } + return@exceptionally null + } */ + .handle { result, e -> + // handleAsync() doesn't work + runOnUiThread { + if (result != null && result is DumpResult) { + mLogText.append("DumpResult - Launcher Extras: ${Utils.dumpExtras(result.launcherExtras)}\n") + } + mLogText.append("Done!\n") + mLogText.append("Result: $result\n") + mLogText.append("Error: $e") + } + } + .thenRunAsync(Runnable { + Log.e(TAG, "Then run ${Thread.currentThread().name}") + mLogText.append("\n") + mLogText.append("Run.") + }, HandlerThreadExecutor(Handler(Looper.getMainLooper()))) + } + rootView.addView(btn) + } + + private interface Callback { + fun call(): CompletableFuture<*> + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b5520df..874bc91 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,10 @@ xmlns:tools="http://schemas.android.com/tools" package="moe.yuuta.workmode"> + + + diff --git a/app/src/main/aidl/moe/yuuta/workmode/IAccessor.aidl b/app/src/main/aidl/moe/yuuta/workmode/IAccessor.aidl new file mode 100644 index 0000000..000ba9d --- /dev/null +++ b/app/src/main/aidl/moe/yuuta/workmode/IAccessor.aidl @@ -0,0 +1,19 @@ +// IAccessor.aidl +package moe.yuuta.workmode; + +import moe.yuuta.workmode.suspend.data.TransferableSuspendedApp; +import moe.yuuta.workmode.access.DumpResult; + +interface IAccessor { + boolean isSuspended(in List packages); + Bundle getSuspendedPackageAppExtras(in TransferableSuspendedApp packageInfo); + Bundle getSuspendedPackageLauncherExtras(in TransferableSuspendedApp packageInfo); + DumpResult dump(in TransferableSuspendedApp packageInfo); + Bundle setPackagesSuspended(in List packages, + boolean suspended, + in PersistableBundle appExtras, + in PersistableBundle launcherExtras, + String dialogMessage); + Bundle apply(in Bundle dat, in TransferableSuspendedApp[] suspendList, int listMode, int status); + List getInstalledApplicationsAcrossUser(int flags); +} diff --git a/app/src/main/aidl/moe/yuuta/workmode/access/DumpResult.aidl b/app/src/main/aidl/moe/yuuta/workmode/access/DumpResult.aidl new file mode 100644 index 0000000..d8bbd3e --- /dev/null +++ b/app/src/main/aidl/moe/yuuta/workmode/access/DumpResult.aidl @@ -0,0 +1,4 @@ +// DumpResult.aidl +package moe.yuuta.workmode.access; + +parcelable DumpResult; \ No newline at end of file diff --git a/app/src/main/aidl/moe/yuuta/workmode/suspend/data/TransferableSuspendedApp.aidl b/app/src/main/aidl/moe/yuuta/workmode/suspend/data/TransferableSuspendedApp.aidl new file mode 100644 index 0000000..58e064f --- /dev/null +++ b/app/src/main/aidl/moe/yuuta/workmode/suspend/data/TransferableSuspendedApp.aidl @@ -0,0 +1,4 @@ +// TransferableSuspendedApp.aidl +package moe.yuuta.workmode.suspend.data; + +parcelable TransferableSuspendedApp; \ No newline at end of file diff --git a/app/src/main/java/moe/yuuta/ext/HandlerThreadExecutor.java b/app/src/main/java/moe/yuuta/ext/HandlerThreadExecutor.java new file mode 100644 index 0000000..d6c51bc --- /dev/null +++ b/app/src/main/java/moe/yuuta/ext/HandlerThreadExecutor.java @@ -0,0 +1,26 @@ +package moe.yuuta.ext; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import androidx.annotation.Nullable; + +import java.util.concurrent.Executor; + +/** + * https://stackoverflow.com/a/21256419/6792243 + */ +public class HandlerThreadExecutor implements Executor { + private final Handler mHandler; + public HandlerThreadExecutor(@Nullable Handler optionalHandler) { + mHandler = optionalHandler != null ? optionalHandler : new Handler(Looper.getMainLooper()); + } + + @Override + public void execute(Runnable command) { + Log.d("Debug", "execute: " + command.toString()); + mHandler.post(command); + Log.d("Debug", "done executing"); + // + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java b/app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java index 7d4cfa8..e909aec 100644 --- a/app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java +++ b/app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java @@ -10,9 +10,12 @@ import android.os.HandlerThread; import android.os.IBinder; import android.os.RemoteException; import android.provider.Settings.Secure; - import com.elvishew.xlog.Logger; import com.elvishew.xlog.XLog; +import moe.yuuta.ext.*; +import moe.yuuta.gplicense.util.Base64; +import moe.yuuta.gplicense.util.Base64DecoderException; +import moe.yuuta.workmode.BuildConfig; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; @@ -20,20 +23,7 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; -import java.util.Date; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.Queue; -import java.util.Set; - -import moe.yuuta.ext.ConnCallback; -import moe.yuuta.ext.IPCResultListener; -import moe.yuuta.ext.IService; -import moe.yuuta.ext.LicServiceConn; -import moe.yuuta.ext.ResultCallback; -import moe.yuuta.gplicense.util.Base64; -import moe.yuuta.gplicense.util.Base64DecoderException; -import moe.yuuta.workmode.BuildConfig; +import java.util.*; /** * Client library for Google Play license verifications. @@ -330,7 +320,7 @@ public class LicenseChecker implements ConnCallback { } /** - * Inform the library that the context is about to be destroyed, so that any open connections + * Inform the library that the hostContext is about to be destroyed, so that any open connections * can be cleaned up. *

* Failure to call this method can result in a crash under certain circumstances, such as during diff --git a/app/src/main/java/moe/yuuta/gplicense/ServerManagedPolicy.java b/app/src/main/java/moe/yuuta/gplicense/ServerManagedPolicy.java index fcc3451..f3b2308 100644 --- a/app/src/main/java/moe/yuuta/gplicense/ServerManagedPolicy.java +++ b/app/src/main/java/moe/yuuta/gplicense/ServerManagedPolicy.java @@ -2,17 +2,15 @@ package moe.yuuta.gplicense; import android.content.Context; import android.content.SharedPreferences; - import com.elvishew.xlog.Logger; import com.elvishew.xlog.XLog; +import moe.yuuta.gplicense.util.URIQueryDecoder; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; -import moe.yuuta.gplicense.util.URIQueryDecoder; - /** * Default policy. All policy decisions are based off of response data received * from the licensing service. Specifically, the licensing server sends the @@ -53,7 +51,7 @@ public class ServerManagedPolicy implements Policy { private PreferenceObfuscator mPreferences; /** - * @param context The context for the current application + * @param context The hostContext for the current application * @param obfuscator An obfuscator to be used with preferences. */ public ServerManagedPolicy(Context context, Obfuscator obfuscator) { diff --git a/app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt b/app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt index a5e3d0c..736fb0a 100644 --- a/app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt +++ b/app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt @@ -3,8 +3,8 @@ package moe.yuuta.workmode import android.app.Activity import android.content.Context import android.content.Intent -import android.content.pm.ApplicationInfo import android.content.pm.PackageManager +import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.os.Bundle import android.util.LruCache @@ -25,23 +25,32 @@ import moe.yuuta.workmode.async.Async import moe.yuuta.workmode.async.Callback import moe.yuuta.workmode.async.Runnable import moe.yuuta.workmode.async.StoppableGroup -import moe.yuuta.workmode.utils.Utils +import moe.yuuta.workmode.suspend.AsyncSuspender +import moe.yuuta.workmode.suspend.data.TransferableSuspendedApp +import java.util.* +import java.util.concurrent.CompletableFuture import java.util.stream.Collectors class ApplicationPickerActivity : AppCompatActivity() { companion object { - const val EXTRA_SELECTED_PACKAGE_NAME = "moe.yuuta.workmode.ApplicationPickerActivity.EXTRA_SELECTED_PACKAGE_NAME" + const val EXTRA_SELECTED_PACKAGES = "moe.yuuta.workmode.ApplicationPickerActivity.EXTRA_SELECTED_PACKAGES" } private val logger: Logger = XLog.tag("ApplicationPickerActivity").build() private lateinit var mAdapter: Adapter - private val mStoppableGroup: StoppableGroup = StoppableGroup() + private lateinit var mLoadAppsFuture: CompletableFuture> private lateinit var mProgressBar: ProgressBar private lateinit var fab: FloatingActionButton - private fun setResultAndFinish(packageNames: Array?) { - setResult(if (packageNames == null) Activity.RESULT_CANCELED else Activity.RESULT_OK, - Intent().putExtra(EXTRA_SELECTED_PACKAGE_NAME, packageNames ?: arrayOf())) + private fun setResultAndFinish(packages: List?) { + val selected = arrayListOf() + selected.addAll(packages ?: listOf()) + selected.stream() + .forEach { + it.trimData() + } + setResult(if (packages == null) Activity.RESULT_CANCELED else Activity.RESULT_OK, + Intent().putExtra(EXTRA_SELECTED_PACKAGES, selected)) finish() } @@ -56,7 +65,14 @@ class ApplicationPickerActivity : AppCompatActivity() { mAdapter = Adapter() recyclerView.adapter = mAdapter fab.setOnClickListener { - setResultAndFinish(mAdapter.checked.toTypedArray()) + setResultAndFinish(mAdapter.data.stream() + .filter { + return@filter it.selected + } + .map { + return@map it.packageInfo + } + .collect(Collectors.toList())) } load() } @@ -72,43 +88,40 @@ class ApplicationPickerActivity : AppCompatActivity() { } private fun load() { - mStoppableGroup.add(Async.beginTask(object : Runnable> { - override fun run(): List { - val selected = intent.getStringArrayExtra(EXTRA_SELECTED_PACKAGE_NAME) ?: arrayOf() - return packageManager.getInstalledApplications(0) - .stream() - .filter(Utils.buildGeneralApplicationInfoFilter(this@ApplicationPickerActivity)) - .sorted(ApplicationInfo.DisplayNameComparator(packageManager)) - .map { - return@map SelectedApp(it.packageName, selected.contains(it.packageName)) - } - .collect(Collectors.toList()) - } - }, object : Callback> { - override fun onStart() { - mProgressBar.visibility = View.VISIBLE - fab.visibility = View.GONE - } - - override fun onStop(success: Boolean, result: List?, e: Throwable?) { - mProgressBar.visibility = View.GONE - fab.visibility = View.VISIBLE - if (success && result != null) { - display(result) - } else { - Toast.makeText(this@ApplicationPickerActivity, + if (::mLoadAppsFuture.isInitialized && !mLoadAppsFuture.isDone) { + mLoadAppsFuture.cancel(true) + } + mLoadAppsFuture = AsyncSuspender(this).getInstalledApplicationsAcrossUser(0) + mProgressBar.visibility = View.VISIBLE + fab.visibility = View.GONE + mLoadAppsFuture + .handle { result, e -> + runOnUiThread { + mProgressBar.visibility = View.GONE + fab.visibility = View.VISIBLE + if (e == null && result != null) { + val selected = intent.getParcelableArrayListExtra(EXTRA_SELECTED_PACKAGES) ?: listOf() + logger.d("Selected: $selected") + if (BuildConfig.DEBUG) logger.d("Installed: $result") + display(result.stream() + .map { + return@map SelectedApp(it, selected.any { a -> return@any a.essentiallyEqual(it) }) + } + .collect(Collectors.toList())) + } else { + Toast.makeText(this@ApplicationPickerActivity, R.string.error_load_applications, Toast.LENGTH_LONG) .show() - if (e != null) { - logger.e("Load applications", e) - } else { - logger.e("Cannot load applications (no stacktrace)") + if (e != null) { + logger.e("Load applications", e) + } else { + logger.e("Cannot load applications (no stacktrace)") + } + // Not sure if the toast will dismiss immediately + // setResultAndFinish() } - // Not sure if the toast will dismiss immediately - // setResultAndFinish() } } - })) } private fun display(result: List) { @@ -125,12 +138,14 @@ class ApplicationPickerActivity : AppCompatActivity() { mAdapter.data[oldItemPosition] == result[newItemPosition] }) - mAdapter.data = result + mAdapter.data = result.toMutableList() diff.dispatchUpdatesTo(mAdapter) } override fun onDestroy() { - mStoppableGroup.stop() + if (::mLoadAppsFuture.isInitialized && !mLoadAppsFuture.isDone) { + mLoadAppsFuture.cancel(true) + } mAdapter.destroy() super.onDestroy() } @@ -139,8 +154,7 @@ class ApplicationPickerActivity : AppCompatActivity() { private val mIconMemoryCaches: LruCache = LruCache(Runtime.getRuntime().maxMemory().toInt() / 5) private val mStoppableGroup: StoppableGroup = StoppableGroup() - internal var data: List = listOf() - internal val checked: MutableSet = mutableSetOf() + internal var data: MutableList = mutableListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH = VH(LayoutInflater.from(parent.context).inflate(R.layout.item_application_select, parent, false)) @@ -150,30 +164,37 @@ class ApplicationPickerActivity : AppCompatActivity() { override fun onBindViewHolder(holder: VH, position: Int) { val context = holder.itemView.context val packageInfo = data[position] - val icon = getIconFromMemoryCache(packageInfo.packageName) + val icon = if (packageInfo.packageInfo.icon != null) BitmapDrawable(context.resources, + packageInfo.packageInfo.icon) + else getIconFromMemoryCache(packageInfo.packageInfo.packageName) if (icon != null) { holder.icon.setImageDrawable(icon) } else { - loadIcon(packageInfo.packageName, holder.itemView.context, holder.icon) + loadIcon(packageInfo.packageInfo.packageName, holder.itemView.context, holder.icon) } - holder.title.text = context - .packageManager - .getApplicationLabel( - context.packageManager.getApplicationInfo(packageInfo.packageName, 0) - ) - if (packageInfo.selected) checked.add(packageInfo.packageName) - else checked.remove(packageInfo.packageName) - holder.checkBox.isChecked = checked.contains(packageInfo.packageName) + holder.title.text = if (packageInfo.packageInfo.label != null) packageInfo.packageInfo.label + else context + .packageManager + .getApplicationLabel( + context.packageManager.getApplicationInfo(packageInfo.packageInfo.packageName, 0) + ) + holder.summary.text = context.getString(R.string.app_list_user_template, packageInfo.packageInfo.userId) + holder.checkBox.isChecked = packageInfo.selected holder.checkBox.setOnClickListener { - val selected = holder.checkBox.isChecked - if (selected) checked.add(packageInfo.packageName) - else checked.remove(packageInfo.packageName) + packageInfo.selected = holder.checkBox.isChecked + Collections.replaceAll(data, + data.stream() + .filter { return@filter it.packageInfo == packageInfo.packageInfo } + .findFirst() + .get(), + packageInfo) } } class VH(itemView: View) : RecyclerView.ViewHolder(itemView) { internal val icon: ImageView = itemView.findViewById(android.R.id.icon) internal val title: TextView = itemView.findViewById(android.R.id.title) + internal val summary: TextView = itemView.findViewById(android.R.id.summary) internal val checkBox: CheckBox = itemView.findViewById(android.R.id.checkbox) } @@ -217,6 +238,6 @@ class ApplicationPickerActivity : AppCompatActivity() { } private data class SelectedApp( - val packageName: String, - val selected: Boolean + val packageInfo: TransferableSuspendedApp, + var selected: Boolean ) \ No newline at end of file diff --git a/app/src/main/java/moe/yuuta/workmode/MainActivity.kt b/app/src/main/java/moe/yuuta/workmode/MainActivity.kt index 1346f6d..587a86a 100644 --- a/app/src/main/java/moe/yuuta/workmode/MainActivity.kt +++ b/app/src/main/java/moe/yuuta/workmode/MainActivity.kt @@ -34,11 +34,10 @@ import moe.yuuta.workmode.async.* import moe.yuuta.workmode.gpl.GPL import moe.yuuta.workmode.suspend.AsyncSuspender import moe.yuuta.workmode.suspend.SuspendTile -import moe.yuuta.workmode.suspend.data.ListMode -import moe.yuuta.workmode.suspend.data.Status -import moe.yuuta.workmode.suspend.data.SuspendedStorage +import moe.yuuta.workmode.suspend.data.* import moe.yuuta.workmode.update.LifecycleUpdateChecker import moe.yuuta.workmode.utils.Utils +import java.util.concurrent.CompletableFuture import java.util.stream.Collectors class MainActivity : AppCompatActivity(), SwitchBar.OnSwitchChangeListener, View.OnClickListener, LicenseCheckerCallback, moe.yuuta.workmode.update.Callback, LifecycleUIUpdateReceiver.Callback { @@ -57,7 +56,7 @@ class MainActivity : AppCompatActivity(), SwitchBar.OnSwitchChangeListener, View private lateinit var mCheckUpdateObserver: LifecycleUpdateChecker - private val mStoppableGroup: StoppableGroup = StoppableGroup() + private lateinit var mApplyFuture: CompletableFuture private var mSortDisplayStoppable: Stoppable? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -85,6 +84,7 @@ class MainActivity : AppCompatActivity(), SwitchBar.OnSwitchChangeListener, View } override fun onSwitchChanged(switchView: Switch?, isChecked: Boolean) { + logger.d("onSwitchChanged $isChecked") SuspendedStorage.get(this).setStatus(if (isChecked) Status.ON else Status.OFF) scheduleApply() } @@ -93,31 +93,34 @@ class MainActivity : AppCompatActivity(), SwitchBar.OnSwitchChangeListener, View * Apply settings which are stored in SuspendedStorage to OS */ private fun scheduleApply() { - mStoppableGroup.add(AsyncSuspender(this).applyFromSettings(object : Callback { - override fun onStart() { - setProgressUI(true) + if (::mApplyFuture.isInitialized && !mApplyFuture.isDone) { + mApplyFuture.cancel(true) + } + mApplyFuture = AsyncSuspender(this) + .applyFromSettings() + setProgressUI(true) + mApplyFuture + .exceptionally { + logger.e("Unable scheduleApply settings", it) + if (Setup.FABRIC_ENABLE) + Crashlytics.getInstance().core.logException(it) + Toast.makeText(this@MainActivity, R.string.error_apply, Toast.LENGTH_LONG).show() } - - override fun onStop(success: Boolean, result: Unit?, e: Throwable?) { + .thenRun { setProgressUI(false) displayUI() - if (!success) { - logger.e("Unable scheduleApply settings", e) - if (Setup.FABRIC_ENABLE) - Crashlytics.getInstance().core.logException(e) - Toast.makeText(this@MainActivity, R.string.error_apply, Toast.LENGTH_LONG).show() - } } - })) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { RC_PICK -> { - if (resultCode == Activity.RESULT_OK && data != null && data.hasExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGE_NAME)) { - val newSet = data.getStringArrayExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGE_NAME).toSet() + if (resultCode == Activity.RESULT_OK && data != null && data.hasExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGES)) { + val newSet = data.getParcelableArrayListExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGES)?.toSet() ?: setOf() logger.d("AR() $newSet") - SuspendedStorage.get(this).setList(newSet) + SuspendedStorage.get(this).setList(newSet.stream() + .map { return@map it.trimToPersistable() } + .collect(Collectors.toList()).toSet()) scheduleApply() } } @@ -132,7 +135,9 @@ class MainActivity : AppCompatActivity(), SwitchBar.OnSwitchChangeListener, View } override fun onDestroy() { - mStoppableGroup.stop() + if (::mApplyFuture.isInitialized && !mApplyFuture.isDone) { + mApplyFuture.cancel(true) + } mAdapter.destroy() if (mSortDisplayStoppable != null) (mSortDisplayStoppable as Stoppable).stop() super.onDestroy() @@ -143,7 +148,10 @@ class MainActivity : AppCompatActivity(), SwitchBar.OnSwitchChangeListener, View */ private fun displayUI() { switchBar.removeOnSwitchChangeListener(this) - switchBar.isChecked = SuspendedStorage.get(this).getStatus() == Status.ON + val checked = SuspendedStorage.get(this).getStatus() == Status.ON + // logger.d("code $code B-C") + if (checked != switchBar.isChecked) switchBar.isChecked = checked + // logger.d("code $code C") switchBar.addOnSwitchChangeListener(this) tabLayout.removeOnTabSelectedListener(mSwitchListModeListener) tabLayout.getTabAt( @@ -158,19 +166,37 @@ class MainActivity : AppCompatActivity(), SwitchBar.OnSwitchChangeListener, View stoppable.stop() mSortDisplayStoppable = null } - mSortDisplayStoppable = Async.beginTask(object : Runnable> { - override fun run(): List? { + mSortDisplayStoppable = Async.beginTask(object : Runnable> { + override fun run(): List? { val sCollator = java.text.Collator.getInstance() return SuspendedStorage.get(this@MainActivity).getList() .stream() .sorted { o1, o2 -> - return@sorted sCollator.compare(packageManager.getApplicationLabel(packageManager.getApplicationInfo(o1, 0)).toString() - , packageManager.getApplicationLabel(packageManager.getApplicationInfo(o2, 0))) + // TODO + val canSafelyLoadAppInfoForO1 = Utils.canSafelyLoadAppInfo(o1, this@MainActivity) + val canSafelyLoadAppInfoForO2 = Utils.canSafelyLoadAppInfo(o2, this@MainActivity) + if (!canSafelyLoadAppInfoForO1 || + !canSafelyLoadAppInfoForO2) { + if (o1.userId > o2.userId) { + return@sorted 1 + } else if (o1.userId < o2.userId) { + return@sorted -1 + } else { + return@sorted 0 + } + } + if ((canSafelyLoadAppInfoForO1 && !canSafelyLoadAppInfoForO2) || + (!canSafelyLoadAppInfoForO1 && canSafelyLoadAppInfoForO2)) { + // A unsafe app is comparing to a safe app, just put the unsafe one in under the safe one. + return@sorted 1 + } + return@sorted sCollator.compare(packageManager.getApplicationLabel(packageManager.getApplicationInfo(o1.packageName, 0)).toString() + , packageManager.getApplicationLabel(packageManager.getApplicationInfo(o2.packageName, 0))) } .collect(Collectors.toList()) } - }, object : Callback> { - override fun onStop(success: Boolean, result: List?, e: Throwable?) { + }, object : Callback> { + override fun onStop(success: Boolean, result: List?, e: Throwable?) { if (result != null) { val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() { override fun getOldListSize(): Int = mAdapter.itemCount @@ -309,9 +335,15 @@ class MainActivity : AppCompatActivity(), SwitchBar.OnSwitchChangeListener, View if (v == null) return when (v.id) { R.id.fab_add -> { + val selected = arrayListOf() + selected.addAll(SuspendedStorage.get(this).getList().stream() + .map { + return@map it.copyToSimpleTransferableInfo() + } + .collect(Collectors.toList())) startActivityForResult(Intent(this, ApplicationPickerActivity::class.java) - .putExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGE_NAME, - SuspendedStorage.get(this).getList().toTypedArray()), RC_PICK) + .putExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGES, + selected), RC_PICK) } } } @@ -367,7 +399,7 @@ private class Adapter : RecyclerView.Adapter() { private val mIconMemoryCaches: LruCache = LruCache(Runtime.getRuntime().maxMemory().toInt() / 5) private val mStoppableGroup: StoppableGroup = StoppableGroup() - internal var data: List = listOf() + internal var data: List = listOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH = VH(LayoutInflater.from(parent.context).inflate(R.layout.item_application, parent, false)) @@ -376,23 +408,31 @@ private class Adapter : RecyclerView.Adapter() { override fun onBindViewHolder(holder: VH, position: Int) { val context = holder.itemView.context - val packageName = data[position] - val icon = getIconFromMemoryCache(packageName) - if (icon != null) { - holder.icon.setImageDrawable(icon) - } else { - loadIcon(packageName, holder.itemView.context, holder.icon) + val packageInfo = data[position] + // TODO + if (Utils.canSafelyLoadAppInfo(packageInfo, context)) { + val icon = getIconFromMemoryCache(packageInfo.packageName) + if (icon != null) { + holder.icon.setImageDrawable(icon) + } else { + loadIcon(packageInfo.packageName, holder.itemView.context, holder.icon) + } } - holder.title.text = context + holder.title.text = if (Utils.canSafelyLoadAppInfo(packageInfo, context)) + context .packageManager .getApplicationLabel( - context.packageManager.getApplicationInfo(packageName, 0) + context.packageManager.getApplicationInfo(packageInfo.packageName, 0) ) + else + packageInfo.packageName + holder.summary.text = context.getString(R.string.app_list_user_template, packageInfo.userId) } class VH(itemView: View) : RecyclerView.ViewHolder(itemView) { internal val icon: ImageView = itemView.findViewById(android.R.id.icon) internal val title: TextView = itemView.findViewById(android.R.id.title) + internal val summary: TextView = itemView.findViewById(android.R.id.summary) } private fun addDrawableToMemoryCache(pkg: String?, icon: Drawable) { diff --git a/app/src/main/java/moe/yuuta/workmode/access/AccessLayer.kt b/app/src/main/java/moe/yuuta/workmode/access/AccessLayer.kt index 31e4622..d9eb12d 100644 --- a/app/src/main/java/moe/yuuta/workmode/access/AccessLayer.kt +++ b/app/src/main/java/moe/yuuta/workmode/access/AccessLayer.kt @@ -2,11 +2,13 @@ package moe.yuuta.workmode.access import android.annotation.SuppressLint import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.IPackageManager import android.content.pm.LauncherApps import android.content.pm.PackageManager import android.os.Bundle +import android.os.Parcel import android.os.PersistableBundle -import android.os.Process import android.os.UserHandle import android.system.Os import androidx.content.pm.PackageOZ @@ -23,16 +25,14 @@ import java.util.concurrent.TimeUnit /** * 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) { +internal class AccessLayer(internal val mContext: Context) { private val mPM: PackageManager = mContext.packageManager @SuppressLint("PrivateApi") fun setPackagesSuspended(packageNames: Array, suspended: Boolean, appExtras: PersistableBundle, launcherExtras: PersistableBundle, - dialogMessage: String): Array { + dialogMessage: String, userId: Int): Array { val countDownLatch = CountDownLatch(1) Thread { // Check installation source and write the result @@ -71,42 +71,31 @@ internal class AccessLayer(private val mContext: Context) { countDownLatch.await(2, TimeUnit.SECONDS) - // ApplicationPackageManager ALWAYS uses context.getOpPackageName() as the argument "callingPackage" + // ApplicationPackageManager ALWAYS uses hostContext.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::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, + val pm = iPM.get(mPM) as IPackageManager + return pm.setPackagesSuspendedAsUser(packageNames, suspended, appExtras, launcherExtras, dialogMessage, BuildConfig.APPLICATION_ID, - UserHandle.getUserHandleForUid(mPM.getPackageUid(mContext.packageName, 0)).hashCode()) as Array + userId) as Array } /** * 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? { + fun getSuspendedPackageAppExtras(packageName: String, userId: Int): PersistableBundle? { Os.setuid(mPM.getPackageUid(packageName, 0)) - // ApplicationPackageManager ALWAYS uses context.getOpPackageName() as the package name + // ApplicationPackageManager ALWAYS uses hostContext.getOpPackageName() as the package name // F**k Google val func: Method = Class.forName("android.content.pm.IPackageManager") .getDeclaredMethod("getSuspendedPackageAppExtras", @@ -119,16 +108,32 @@ internal class AccessLayer(private val mContext: Context) { return func.invoke(iPM.get(mPM), packageName, - UserHandle.getUserHandleForUid(mPM.getPackageUid(packageName, 0)).hashCode()) as PersistableBundle? + userId) 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 isPackageSuspended(packageName: String, userId: Int): Boolean { + val func: Method = PackageManager::class.java.getDeclaredMethod("isPackageSuspendedForUser", + String::class.java, + Int::class.java) + return func.invoke(mPM, packageName, userId) as Boolean } - fun getSuspendedPackageLauncherExtras(packageName: String): Bundle? = - mContext.getSystemService(LauncherApps::class.java).getSuspendedPackageLauncherExtras(packageName, Process.myUserHandle()) + fun getSuspendedPackageLauncherExtras(packageName: String, userId: Int): Bundle? = + mContext.getSystemService(LauncherApps::class.java).getSuspendedPackageLauncherExtras(packageName, + createUserHandleWithUserID(userId)) + + fun getInstalledApplicationsAsUser(flags: Int, userId: Int): List = + mPM.getInstalledApplicationsAsUser(flags, userId) + + companion object { + fun createUserHandleWithUserID(userId: Int): UserHandle { + val parcel = Parcel.obtain() + parcel.writeInt(userId) + // I bet that it won't change a lot + val userHandle = UserHandle(parcel) + parcel.recycle() + return userHandle + } + } } \ No newline at end of file diff --git a/app/src/main/java/moe/yuuta/workmode/access/AccessLayerUtil.kt b/app/src/main/java/moe/yuuta/workmode/access/AccessLayerUtil.kt new file mode 100644 index 0000000..4bfee58 --- /dev/null +++ b/app/src/main/java/moe/yuuta/workmode/access/AccessLayerUtil.kt @@ -0,0 +1,102 @@ +package moe.yuuta.workmode.access + +import android.content.pm.ApplicationInfo +import android.os.BaseBundle +import android.os.Bundle +import android.os.PersistableBundle +import android.os.UserManager +import com.elvishew.xlog.XLog +import moe.yuuta.workmode.R +import moe.yuuta.workmode.suspend.data.TransferableSuspendedApp +import moe.yuuta.workmode.utils.BundleUtils +import java.util.stream.Collectors + +/** + * Provides some quick accesses of AccessLayer + */ +internal class AccessLayerUtil(private val accessLayer: AccessLayer) { + private val logger = XLog.tag("ALUtil").build() + + fun collectUserIDs(packages: List): Map> { + // Collect them with specified user ids + val packagesWithUserIds = mutableMapOf>() + packages.stream() + .forEach { + val list = packagesWithUserIds[it.userId]?.toMutableList() ?: mutableListOf() + list.add(it) + packagesWithUserIds[it.userId] = list + } + return packagesWithUserIds + } + + fun suspend(packages: List, + suspended: Boolean, + appExtras: BaseBundle, + launcherExtras: BaseBundle, + dialogMessage: String): Array { + val packagesWithUserIds = collectUserIDs(packages) + + val result = mutableListOf() + for (userId in packagesWithUserIds.keys) { + result.addAll(accessLayer.setPackagesSuspended( + packagesWithUserIds[userId]!!.stream() + .map { return@map it.packageName } + .collect(Collectors.toList()).toTypedArray(), + suspended, + when (appExtras) { + is PersistableBundle -> appExtras + is Bundle -> BundleUtils.toPersistableBundle(appExtras) + else -> PersistableBundle() + }, + when (launcherExtras) { + is PersistableBundle -> launcherExtras + is Bundle -> BundleUtils.toPersistableBundle(launcherExtras) + else -> PersistableBundle() + }, dialogMessage, userId)) + } + return result.toTypedArray() + } + + fun suspend(packages: List, suspended: Boolean): Array = + suspend(packages, + suspended, + Bundle(), + PersistableBundle(), // Removed because there is an unknown bug which prevents from writing launcher extras from the owner (?) + accessLayer.mContext.getString(R.string.suspended_message)) + + fun getSuspendedPackageAppExtras(packageInfo: TransferableSuspendedApp): PersistableBundle? = + accessLayer.getSuspendedPackageAppExtras(packageName = packageInfo.packageName, + userId = packageInfo.userId) + + fun isPackageSuspended(packageInfo: TransferableSuspendedApp): Boolean = + accessLayer.isPackageSuspended(packageName = packageInfo.packageName, + userId = packageInfo.userId) + + fun getSuspendedPackageLauncherExtras(packageInfo: TransferableSuspendedApp): Bundle? = + accessLayer.getSuspendedPackageLauncherExtras(packageName = packageInfo.packageName, + userId = packageInfo.userId) + + fun getInstalledApplicationsAsUser(flags: Int, userId: Int): List = + accessLayer.getInstalledApplicationsAsUser(flags, userId) + + fun getInstalledApplicationsAnyUser(flags: Int): List { + val userManager = accessLayer.mContext.getSystemService(UserManager::class.java) + val finalResult = mutableListOf() + val ids = userManager.getUsers(false) + .stream() + .map { + return@map it.id + } + .collect(Collectors.toList()) + .toTypedArray() + + for (userHandle in ids) { + finalResult.addAll(getInstalledApplicationsAsUser(flags, + userHandle.hashCode())) + } + // val result = accessLayer.getInstalledApplicationsAsUser(flags or + // PackageManager::class.java.getDeclaredField("MATCH_ANY_USER").getInt(null), + // 0 /* System */) + return finalResult + } +} \ 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 index 18f19d0..ac198d9 100644 --- a/app/src/main/java/moe/yuuta/workmode/access/AccessorStarter.kt +++ b/app/src/main/java/moe/yuuta/workmode/access/AccessorStarter.kt @@ -1,231 +1,194 @@ package moe.yuuta.workmode.access +import android.content.ComponentName import android.content.Context import android.os.Bundle -import android.os.Parcel import android.os.PersistableBundle +import android.service.quicksettings.TileService +import androidx.annotation.WorkerThread import com.elvishew.xlog.Logger import com.elvishew.xlog.XLog +import eu.chainfire.librootjava.RootIPCReceiver import eu.chainfire.librootjava.RootJava import eu.chainfire.libsuperuser.Shell import moe.yuuta.workmode.BuildConfig +import moe.yuuta.workmode.IAccessor +import moe.yuuta.workmode.R +import moe.yuuta.workmode.Setup.getLogsPath +import moe.yuuta.workmode.suspend.SuspendTile import moe.yuuta.workmode.suspend.data.ListMode import moe.yuuta.workmode.suspend.data.Status import moe.yuuta.workmode.suspend.data.SuspendedStorage -import moe.yuuta.workmode.utils.ByteArraySerializer +import moe.yuuta.workmode.suspend.data.TransferableSuspendedApp +import java.util.function.Function import java.util.stream.Collectors /** * 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() - +open class AccessorStarter(private val mContext: Context, private val mService: IAccessor, private val mListener: RootIPCReceiver) { companion object { + private val logger: Logger = XLog.tag("AccessorStarter").build() + 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 { - val command = RootJava.getLaunchScript(mContext, + // #Anti-Crack + internal const val EXTRA_ERROR_CODE = "moe.yuuta.workmode.access.EXTRA_ERROR_CODE" + internal const val EXTRA_ERROR_MSG = "moe.yuuta.workmode.access.EXTRA_ERROR_MSG" + internal const val EXTRA_ERROR_STATUS = "moe.yuuta.workmode.access.EXTRA_ERROR_STATUS" + internal const val EXTRA_DATA = "moe.yuuta.workmode.access.EXTRA_DATA" + internal const val EXTRA_DAT = "moe.yuuta.workmode.access.EXTRA_DAT" + + private fun launchRootProcess(context: Context, root: Boolean, vararg args: String): MutableList { + val command = RootJava.getLaunchScript(context, WorkModeAccessor::class.java, null, null, args, BuildConfig.APPLICATION_ID + ":accessor") - return if (root) { - Shell.SU.run(command) - } else { - Shell.SH.run(command) + return if (root) { + Shell.SU.run(command) + } else { + Shell.SH.run(command) + } } - } - fun getSuspendedPackageAppExtras(packageName: String, root: Boolean): Bundle? { - val argumentParcel: Parcel = obtainArgumentParcel() - try { - 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() - } - } + /** + * Create a connected Starter object. + */ + @WorkerThread + fun start(context: Context, root: Boolean, thenRun: Function): T { + var res: T? = null + var err: Throwable? = null + object : RootIPCReceiver(context, 0x302) { + override fun onConnect(ipc: IAccessor?) { + logger.d("Connected to the system") + val starter = AccessorStarter(context, ipc!!, this) + try { + res = thenRun.apply(starter) + } catch (e: Throwable) { + logger.d("Cannot perform the action", e) + err = e + } + starter.release() + } - fun getSuspendedPackageLauncherExtras(packageName: String, root: Boolean): Bundle? { - val argumentParcel: Parcel = obtainArgumentParcel() - try { - - 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() + override fun onDisconnect(ipc: IAccessor?) { + logger.d("Disconnected from the system") + } + } + logger.d("Starting root process.....") + launchRootProcess(context, root, getLogsPath(context).absolutePath) + // Wait until it exits. + // We assume that the server must return a non-null result or an exception. + if (err != null) + throw err as Throwable + return res!! } } - fun isPackageSuspended(packageNames: Array, root: Boolean): Boolean { - val argumentParcel: Parcel = obtainArgumentParcel() - try { - - 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 getSuspendedPackageAppExtras(packageInfo: TransferableSuspendedApp): Bundle? = + mService.getSuspendedPackageAppExtras(packageInfo) - fun dump(packageName: String, root: Boolean): DumpResult { - val argumentParcel: Parcel = obtainArgumentParcel() - try { - - 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 getSuspendedPackageLauncherExtras(packageInfo: TransferableSuspendedApp): Bundle? = + mService.getSuspendedPackageLauncherExtras(packageInfo) + + fun isPackageSuspended(packages: List): Boolean { + return mService.isSuspended(packages) } - fun setPackagesSuspended(packageNames: Array, suspended: Boolean, + fun dump(packageInfo: TransferableSuspendedApp): DumpResult = + mService.dump(packageInfo) + + fun setPackagesSuspended(packages: List, suspended: Boolean, appExtras: PersistableBundle, launcherExtras: PersistableBundle, - dialogMessage: String, root: Boolean): Array { - val argumentParcel: Parcel = obtainArgumentParcel() - try { - - 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() - } + dialogMessage: String): Array { + val result = mService.setPackagesSuspended(packages, suspended, appExtras, launcherExtras, dialogMessage) + processError(result) + return result.getStringArray(EXTRA_DATA) } - @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") + // Read the Bundle which is returned from some methods. + // It contains the crack information and the normal information. + // If it has crack information, log it. + // #Anti-Crack + private fun processError(bundle: Bundle) { + when (bundle.getInt(EXTRA_ERROR_CODE)) { + 1 -> { } // If server returns this code, which means the task is successfully executed but // it had detected that the app was cracked. // #Anti-Crack - 2.toByte() -> { + 2 -> { // The ID is used to prevent from multiple reporting. - val id = result.readString() - val reason = result.readString() + val id = bundle.getString(EXTRA_ERROR_STATUS) + val reason = bundle.getString(EXTRA_ERROR_MSG) SuspendedStorage.get(mContext).reportCrack(id ?: "nd", reason ?: "nr") } } - return result - } - - fun getPackagesSuspendedByWorkMode(root: Boolean): List { - val argumentParcel: Parcel = obtainArgumentParcel() - try { - - 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, listMode: ListMode, status: Status, root: Boolean) { - val argumentParcel: Parcel = obtainArgumentParcel() - try { - - 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() - } - } - - private fun obtainArgumentParcel(): Parcel { - val argumentParcel: Parcel = Parcel.obtain() - argumentParcel.writeString(mLogPath) + fun apply(suspendList: Array, listMode: ListMode, status: Status) { // Tell the trigger times and times to the server, it will disable the app automatically // #Anti-Crack val sp = SuspendedStorage.get(mContext).getStorage() val keys = sp.all.keys.stream() - .filter { - return@filter it.startsWith("c_") - } - .collect(Collectors.toList()) - val map = mutableMapOf() + .filter { + return@filter it.startsWith("c_") + } + .collect(Collectors.toList()) + val map = hashMapOf() for (key in keys) { try { val times = sp.getInt(key, -1) map[key] = times } catch (e: Throwable) {} } - argumentParcel.writeMap(map) - return argumentParcel + val dat = Bundle() + dat.putSerializable(EXTRA_DAT, map) + val result = mService.apply(dat, suspendList, + when (listMode) { + ListMode.BLACKLIST -> 1 + ListMode.WHITELIST -> 2 + }, + when (status) { + Status.ON -> 1 + Status.OFF -> 2 + }) + processError(result) + } + + fun getInstalledApplicationsAcrossUser(flags: Int): List = + mService.getInstalledApplicationsAcrossUser(flags) + + fun release() { + mListener.release() + } + + fun isConnected(): Boolean = mService.asBinder().isBinderAlive + + fun suspend(packages: List, suspended: Boolean): Array = + setPackagesSuspended(packages, + suspended, + PersistableBundle(), + PersistableBundle(), // Removed because there is an unknown bug which prevents from writing launcher extras from the owner (?) + mContext.getString(R.string.suspended_message)) + + fun apply() { + val storage = SuspendedStorage.get(mContext) + val status = storage.getStatus() + val listMode = storage.getListMode() + val list = storage.getList() + .stream() + .map { + return@map it.copyToSimpleTransferableInfo() + } + .collect(Collectors.toList()) + apply(list.toTypedArray(), listMode, status) + TileService.requestListeningState(mContext, ComponentName(mContext, SuspendTile::class.java)) } } \ 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 deleted file mode 100644 index 8715989..0000000 --- a/app/src/main/java/moe/yuuta/workmode/access/ApplicationAccessorStarter.kt +++ /dev/null @@ -1,9 +0,0 @@ -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 index d4a9d79..e544e7d 100644 --- a/app/src/main/java/moe/yuuta/workmode/access/DumpResult.kt +++ b/app/src/main/java/moe/yuuta/workmode/access/DumpResult.kt @@ -1,7 +1,32 @@ package moe.yuuta.workmode.access import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable data class DumpResult(val isSuspended: Boolean, - val appExtras: Bundle?, - val launcherExtras: Bundle?) \ No newline at end of file + val launcherExtras: Bundle?) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readByte() != 0.toByte(), + parcel.readBundle(Bundle::class.java.classLoader) + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeByte(if (isSuspended) 1 else 0) + parcel.writeBundle(launcherExtras) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): DumpResult { + return DumpResult(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/yuuta/workmode/access/HostInfo.kt b/app/src/main/java/moe/yuuta/workmode/access/HostInfo.kt new file mode 100644 index 0000000..05b1622 --- /dev/null +++ b/app/src/main/java/moe/yuuta/workmode/access/HostInfo.kt @@ -0,0 +1,25 @@ +package moe.yuuta.workmode.access + +import android.content.Context +import android.os.Binder +import moe.yuuta.workmode.BuildConfig +import moe.yuuta.workmode.Manifest + +internal data class HostInfo( + val userId: Int, + val hostContext: Context +) { + companion object { + + /** + * Generate the HostInfo from API caller. If it is NOT an authorized host, throw a SecurityException. + */ + @Throws(SecurityException::class) + internal fun getHostInfoFromCaller(systemContext: Context): HostInfo { + val user = Binder.getCallingUserHandle() + systemContext.enforceCallingPermission(Manifest.permission.ACCESS, null) + return HostInfo(user.hashCode(), + systemContext.createPackageContextAsUser(BuildConfig.APPLICATION_ID, 0, user)) + } + } +} \ 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 deleted file mode 100644 index 05c4983..0000000 --- a/app/src/main/java/moe/yuuta/workmode/access/ShellAccessorStarter.kt +++ /dev/null @@ -1,9 +0,0 @@ -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 index 57ec307..5f50d67 100644 --- a/app/src/main/java/moe/yuuta/workmode/access/WorkModeAccessor.kt +++ b/app/src/main/java/moe/yuuta/workmode/access/WorkModeAccessor.kt @@ -1,42 +1,41 @@ 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.os.* +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 com.elvishew.xlog.Logger import com.elvishew.xlog.XLog +import eu.chainfire.librootjava.RootIPC import eu.chainfire.librootjava.RootJava import eu.chainfire.libsuperuser.Shell 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.SuspendedApp 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.utils.BundleUtils -import moe.yuuta.workmode.utils.ByteArraySerializer +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 { - 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() @@ -45,230 +44,307 @@ class WorkModeAccessor { } private lateinit var logger: Logger - private lateinit var mContext: Context - private lateinit var pmAccess: AccessLayer + private lateinit var mSystemContext: Context 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" + 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 { - // 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) { - // Only self-uninstall if user usually use the app. - val usageLevel = getAppStandbyBucket(BuildConfig.APPLICATION_ID, mContext) - if (usageLevel != UsageStatsManager.STANDBY_BUCKET_FREQUENT) { - 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 - } else { - logger.d("uL = $usageLevel, skipping.") - } - } - } - // 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)) + 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) } - 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) + hostContext.sendBroadcast(Intent(AccessorStarter.ACTION_UPDATE_UI_PROGRESS) + .putExtra(AccessorStarter.EXTRA_SHOW_PROGRESS, false)) } - 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 + // 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.") } - 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)) + } + } + + 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 } - 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) + .collect(Collectors.toList()) + result.forEach { + logger.d("SuspendingA-A ${it.packageName} ${it.userId}") + } + return result + } + + private fun _getInstalledApplicationsAcrossUser(pmAccess: AccessLayerUtil, hostInfo: HostInfo, flags: Int): MutableList { + val originalApplicationInfo = mutableMapOf() + val packages = pmAccess.getInstalledApplicationsAnyUser(flags).stream() + .map { + val sus = fillDataIfNeeded(PersistableSuspendedApp(UserHandle.getUserHandleForUid(it.uid).hashCode(), + it.packageName), hostInfo) + originalApplicationInfo.put(sus, it) + return@map sus } - ACTION_GET_ALL_PACKAGES_SUSPENDED_BY_WORK_MODE -> { - parcel.writeStringList(getPackagesSuspendedByWorkMode()) + .collect(Collectors.toList()) + 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()) } - ACTION_APPLY -> { - apply(argsParcel) + .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 } - } - 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 - ) + 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 + } - 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()) + 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)) + logger.d("Installed applications (all users, debug): ${pmAccess.getInstalledApplicationsAnyUser(0)}") + 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)) + readErrors(result, hostContext = hostInfo.hostContext) + postExecuteNotify(hostContext = hostInfo.hostContext) + return result + } - 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 - // } + 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. + // 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) + // 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") + } + + systemSuspendedList .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") - } + .forEach { + logger.d("SYS ${it.packageName} ${it.userId}") + } - val tasks = systemAllAppList.stream() + 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) + 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) { + logger.d("Ignoring $systemSuspended $inOurList ${it.packageName} (${it.userId}) because of A") return@filter false } else { + logger.d("Including $systemSuspended $inOurList ${it.packageName} (${it.userId}) because of B") return@filter true } } else { if (status == Status.ON) { + logger.d("Including $systemSuspended $inOurList ${it.packageName} (${it.userId}) because of C") return@filter true } else { + logger.d("Ignoring $systemSuspended $inOurList ${it.packageName} (${it.userId}) because of D") return@filter false } } } else { if (status == Status.ON) { + logger.d("Ignoring/Including $systemSuspended $inOurList ${it.packageName} (${it.userId}) because of E ($inOurList)") return@filter inOurList } else { + logger.d("Ignoring $systemSuspended $inOurList ${it.packageName} (${it.userId}) because of F") return@filter false } } @@ -276,18 +352,23 @@ class WorkModeAccessor { ListMode.WHITELIST -> { if (systemSuspended) { if (inOurList) { + logger.d("Including $systemSuspended $inOurList ${it.packageName} (${it.userId}) because of B1") return@filter true } else { if (status == Status.ON) { + logger.d("Ignoring $systemSuspended $inOurList ${it.packageName} (${it.userId}) because of B2") return@filter false } else { + logger.d("Including $systemSuspended $inOurList ${it.packageName} (${it.userId}) because of B3") return@filter true } } } else { if (status == Status.ON) { + logger.d("Ignoring/Including $systemSuspended $inOurList ${it.packageName} (${it.userId}) because of B4 (${!inOurList})") return@filter !inOurList } else { + logger.d("Ignoring $systemSuspended $inOurList ${it.packageName} (${it.userId}) because of B5") return@filter false } } @@ -296,7 +377,9 @@ class WorkModeAccessor { } // Now, map them and determine that whatever a package should be suspended or un-suspended. .map { - val systemSuspended = systemSuspendedList.contains(it) + val systemSuspended = systemSuspendedList.any { a -> + return@any a.essentiallyEqual(it) + } when (listMode) { ListMode.BLACKLIST -> { if (systemSuspended) { @@ -322,46 +405,53 @@ class WorkModeAccessor { } // Collect them, we will execute later. .collect(Collectors.toList()) - // Suspend first - if (status == Status.ON) { - val suspendList = tasks.stream() + // Suspend first + if (status == Status.ON) { + val suspendList = tasks.stream() .filter { return@filter it.suspend } .map { - return@map it.packageName + return@map it.packageInfo } .collect(Collectors.toList()) - if (suspendList.size > 0) { - suspend(suspendList.toTypedArray(), + logger.d("Applying settings: suspend $suspendList") + if (suspendList.size > 0) { + pmAccess.suspend(suspendList, true) + } } - } - // Then unsuspand - val unsuspendList = tasks.stream() + // Then unsuspand + val unsuspendList = tasks.stream() .filter { return@filter !it.suspend } .map { - return@map it.packageName + return@map it.packageInfo } .collect(Collectors.toList()) - if (unsuspendList.size > 0) { - suspend(unsuspendList.toTypedArray(), + logger.d("Applying settings: unsuspend $unsuspendList") + if (unsuspendList.size > 0) { + pmAccess.suspend(unsuspendList, false) + } + readErrors(result, hostContext = hostInfo.hostContext) + postExecuteNotify(hostContext = hostInfo.hostContext) + return result } - } - 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)) + 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 packageName: String, + val packageInfo: TransferableSuspendedApp, val suspend: Boolean ) diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/AsyncSuspender.kt b/app/src/main/java/moe/yuuta/workmode/suspend/AsyncSuspender.kt index 8cb4947..676d0ed 100644 --- a/app/src/main/java/moe/yuuta/workmode/suspend/AsyncSuspender.kt +++ b/app/src/main/java/moe/yuuta/workmode/suspend/AsyncSuspender.kt @@ -1,27 +1,51 @@ package moe.yuuta.workmode.suspend import android.content.Context -import moe.yuuta.workmode.async.Async -import moe.yuuta.workmode.async.Callback -import moe.yuuta.workmode.async.Runnable -import moe.yuuta.workmode.async.Stoppable +import android.os.Bundle +import moe.yuuta.workmode.access.AccessorStarter +import moe.yuuta.workmode.access.DumpResult +import moe.yuuta.workmode.suspend.data.TransferableSuspendedApp +import java.util.concurrent.CompletableFuture +import java.util.function.Function +import java.util.function.Supplier /** * An async suspender which wraps suspend tasks and run them in the background */ class AsyncSuspender(private val mContext: Context) { - fun suspend(packageNames: Array, suspended: Boolean, callback: Callback>): Stoppable = - Async.beginTask(object : Runnable> { - override fun run(): Array = Suspender(mContext).suspend(packageNames, suspended) - }, callback) - - fun isSuspended(packageNames: Array, callback: Callback): Stoppable = - Async.beginTask(object : Runnable { - override fun run(): Boolean = Suspender(mContext).isSuspended(packageNames) - }, callback) - - fun applyFromSettings(callback: Callback): Stoppable = - Async.beginTask(object : Runnable { - override fun run(): Unit = Suspender(mContext).applyFromSettings() - }, callback) + fun isSuspended(packages: List): CompletableFuture = generalCall(Function { + return@Function it.isPackageSuspended(packages) + }) + + fun suspend(packages: List, suspended: Boolean): CompletableFuture> = generalCall( + Function { + return@Function it.suspend(packages, suspended) + }) + + fun getInstalledApplicationsAcrossUser(flags: Int): CompletableFuture> = generalCall( + Function { + return@Function it.getInstalledApplicationsAcrossUser(flags) + }) + + private fun generalCall(thenApply: java.util.function.Function): CompletableFuture { + return CompletableFuture.supplyAsync(Supplier { + return@Supplier AccessorStarter.start(mContext, true, thenApply) + }) + } + + fun applyFromSettings(): CompletableFuture = generalCall(Function { t -> t.apply() }) + + fun getSuspendedPackageAppExtras(packageInfo: TransferableSuspendedApp): CompletableFuture = generalCall(Function { + return@Function it.getSuspendedPackageAppExtras(packageInfo) + }) + + fun getSuspendedPackageLauncherExtras(packageInfo: TransferableSuspendedApp): CompletableFuture = generalCall(Function { + return@Function it.getSuspendedPackageLauncherExtras(packageInfo) + }) + + fun dump(packageInfo: TransferableSuspendedApp): CompletableFuture = generalCall(Function { + return@Function it.dump(packageInfo) + }) + + private data class DataWrapper(val accessorStarter: AccessorStarter?, val data: T?) } \ No newline at end of file diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/SuspendTile.kt b/app/src/main/java/moe/yuuta/workmode/suspend/SuspendTile.kt index 23a3334..75b6ce4 100644 --- a/app/src/main/java/moe/yuuta/workmode/suspend/SuspendTile.kt +++ b/app/src/main/java/moe/yuuta/workmode/suspend/SuspendTile.kt @@ -3,10 +3,13 @@ package moe.yuuta.workmode.suspend import android.content.Intent import android.service.quicksettings.Tile import android.service.quicksettings.TileService +import com.elvishew.xlog.XLog import moe.yuuta.workmode.suspend.data.Status import moe.yuuta.workmode.suspend.data.SuspendedStorage +import java.util.concurrent.TimeUnit class SuspendTile : TileService() { + private val logger = XLog.tag("SuspendTile").build() override fun onClick() { val storage = SuspendedStorage.get(this) storage.setStatus( @@ -16,7 +19,12 @@ class SuspendTile : TileService() { } ) sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) - Suspender(this).applyFromSettings() + try { + AsyncSuspender(this).applyFromSettings() + .get(10, TimeUnit.SECONDS) + } catch (e: Throwable) { + logger.e("Cannot trigger", e) + } } override fun onStartListening() { diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/SuspendedApp.kt b/app/src/main/java/moe/yuuta/workmode/suspend/SuspendedApp.kt deleted file mode 100644 index 9c6ddf7..0000000 --- a/app/src/main/java/moe/yuuta/workmode/suspend/SuspendedApp.kt +++ /dev/null @@ -1,38 +0,0 @@ -package moe.yuuta.workmode.suspend - -import android.os.Bundle -import android.os.PersistableBundle -import moe.yuuta.workmode.BuildConfig - -/** - * The data class of a suspended app. This is ONLY used in this app, which - * can be understood as "a bridge between the extras stored in the system and - * information which is used in this app" - */ -data class SuspendedApp( - val isSuspendedByWorkMode: Boolean, // The flag which is used to determine whatever is suspended by WorkMode - val versionCodeSuspended: Int // The version code of this app when suspended the target -) { - companion object { - // These values are stored by the system, should not be usually changed for migrating - const val EXTRA_IS_SUSPENDED_BY_WORK_MODE = "moe.yuuta.workmode.EXTRA_IS_SUSPENDED_BY_WORK_MODE" - const val EXTRA_VERSION_CODE = "moe.yuuta.workmode.EXTRA_VERSION_CODE" - - fun deserializeBundle(launcherExtras: Bundle?): SuspendedApp { - if (launcherExtras == null) return SuspendedApp(false, -1) - return SuspendedApp( - launcherExtras.getBoolean(EXTRA_IS_SUSPENDED_BY_WORK_MODE, false), - launcherExtras.getInt(EXTRA_VERSION_CODE, -1) - ) - } - - fun getDefault(): SuspendedApp = SuspendedApp(true, BuildConfig.VERSION_CODE) - } - - fun serializeBundle(): PersistableBundle { - val bundle = PersistableBundle() - bundle.putBoolean(EXTRA_IS_SUSPENDED_BY_WORK_MODE, isSuspendedByWorkMode) - bundle.putInt(EXTRA_VERSION_CODE, versionCodeSuspended) - return bundle - } -} \ No newline at end of file diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/Suspender.kt b/app/src/main/java/moe/yuuta/workmode/suspend/Suspender.kt deleted file mode 100644 index 504fdba..0000000 --- a/app/src/main/java/moe/yuuta/workmode/suspend/Suspender.kt +++ /dev/null @@ -1,40 +0,0 @@ -package moe.yuuta.workmode.suspend - -import android.content.ComponentName -import android.content.Context -import android.os.PersistableBundle -import android.service.quicksettings.TileService -import moe.yuuta.workmode.R -import moe.yuuta.workmode.access.ApplicationAccessorStarter -import moe.yuuta.workmode.suspend.data.SuspendedStorage - -/** - * The highest-level suspender to wrap all information needed to suspend or vice versa. This - * should be called from UI components directly - * Chain: UI -> Suspender -> AccessorStarter -> WorkModeAccessor -> AccessLayer -> Framework - */ -class Suspender(private val mContext: Context) { - fun suspend(packageNames: Array, suspended: Boolean): Array = - ApplicationAccessorStarter(mContext).setPackagesSuspended(packageNames, - suspended, - PersistableBundle(), - SuspendedApp.getDefault().serializeBundle(), // We use LauncherExtras because they are easy to read - mContext.getString(R.string.suspended_message), - true) - - fun isSuspended(packageNames: Array): Boolean = - ApplicationAccessorStarter(mContext).isPackageSuspended(packageNames, true) - - fun getPackagesSuspendedByWorkMode(): List = - ApplicationAccessorStarter(mContext).getPackagesSuspendedByWorkMode(true) - - fun applyFromSettings() { - val storage = SuspendedStorage.get(mContext) - storage.cleanList(mContext) - val status = storage.getStatus() - val listMode = storage.getListMode() - val list = storage.getList() - ApplicationAccessorStarter(mContext).apply(list.toTypedArray(), listMode, status, true) - TileService.requestListeningState(mContext, ComponentName(mContext, SuspendTile::class.java)) - } -} \ No newline at end of file diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/data/PersistableSuspendedApp.kt b/app/src/main/java/moe/yuuta/workmode/suspend/data/PersistableSuspendedApp.kt new file mode 100644 index 0000000..ccfc008 --- /dev/null +++ b/app/src/main/java/moe/yuuta/workmode/suspend/data/PersistableSuspendedApp.kt @@ -0,0 +1,41 @@ +package moe.yuuta.workmode.suspend.data + +import android.os.Process +import java.util.regex.Pattern + +/** + * The data of Suspended app which can be stored in {@link SuspendedStorage} only + */ +data class PersistableSuspendedApp( + val userId: Int, + val packageName: String +) { + constructor(serializedString: String) : this(parseUserIDFromSerializedString(serializedString), + parsePackageNameFromSerializedString(serializedString)) + + override fun toString(): String = + String.format("%d|%s", userId, packageName) + + /** + * Create a TransferableSuspendedApp with the simplest data. + */ + fun copyToSimpleTransferableInfo(): TransferableSuspendedApp = + TransferableSuspendedApp(userId, packageName, null, -1, null) + + companion object { + private fun parseFromSerializedString(serializedString: String): PersistableSuspendedApp { + val arr = serializedString.split(Pattern.compile("\\|")) + // Legacy + if (arr.size != 2) { + return PersistableSuspendedApp(Process.myUserHandle().hashCode(), serializedString) + } + return PersistableSuspendedApp(arr[0].toInt(), arr[1]) + } + + private fun parseUserIDFromSerializedString(serializedString: String): Int = + parseFromSerializedString(serializedString).userId + + private fun parsePackageNameFromSerializedString(serializedString: String): String = + parseFromSerializedString(serializedString).packageName + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/data/SuspendedStorage.kt b/app/src/main/java/moe/yuuta/workmode/suspend/data/SuspendedStorage.kt index 74c9b67..af3cd87 100644 --- a/app/src/main/java/moe/yuuta/workmode/suspend/data/SuspendedStorage.kt +++ b/app/src/main/java/moe/yuuta/workmode/suspend/data/SuspendedStorage.kt @@ -7,7 +7,6 @@ import com.crashlytics.android.answers.Answers import com.crashlytics.android.answers.CustomEvent import com.elvishew.xlog.XLog import moe.yuuta.workmode.Setup -import moe.yuuta.workmode.utils.Utils import java.util.stream.Collectors /** @@ -31,9 +30,27 @@ class SuspendedStorage(mContext: Context) { fun getStorage(): SharedPreferences = storage - fun getList(): List = (getStorage().getStringSet("list", setOf()) ?: listOf()).toList() + fun getList(): List = + _getList().stream() + .map { + return@map PersistableSuspendedApp(it) + } + .collect(Collectors.toList()) - fun setList(set: Set) { + fun setList(set: Set) { + _setList(set.stream() + .map { + return@map it.toString() + } + .collect(Collectors.toSet())) + } + + /** + * Internal get list (string) + */ + private fun _getList(): List = (getStorage().getStringSet("list", setOf()) ?: listOf()).toList() + + private fun _setList(set: Set) { logger.d("s() $set") getStorage().edit().putStringSet("list", set).apply() } @@ -68,21 +85,6 @@ class SuspendedStorage(mContext: Context) { else -> ListMode.BLACKLIST } - fun cleanList(context: Context) { - val installed = context.packageManager.getInstalledApplications(0) - .stream() - .filter(Utils.buildGeneralApplicationInfoFilter(context)) - .map { - return@map it.packageName - } - .collect(Collectors.toList()) - setList(getList().stream() - .filter { - return@filter installed.contains(it) - } - .collect(Collectors.toSet())) - } - // #Anti-Crack fun reportCrack(id: String, reason: String) { if (getList().isEmpty()) { diff --git a/app/src/main/java/moe/yuuta/workmode/suspend/data/TransferableSuspendedApp.kt b/app/src/main/java/moe/yuuta/workmode/suspend/data/TransferableSuspendedApp.kt new file mode 100644 index 0000000..03bf842 --- /dev/null +++ b/app/src/main/java/moe/yuuta/workmode/suspend/data/TransferableSuspendedApp.kt @@ -0,0 +1,101 @@ +package moe.yuuta.workmode.suspend.data + +import android.annotation.SystemApi +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.Parcel +import android.os.Parcelable +import android.os.Process +import android.text.TextUtils +import com.elvishew.xlog.XLog + +/** + * The data of Suspended app which can be transfered through IPC only + */ +data class TransferableSuspendedApp( + val userId: Int, + val packageName: String, + // Null if it has the same user id with the host + var label: CharSequence?, + // -1 if not available + var uid: Int, + // Null if it has the same user id with the host + var icon: Bitmap? +) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readInt(), + parcel.readString(), + TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel), + parcel.readInt(), + parcel.readParcelable(Bitmap::class.java.classLoader) + ) + + fun trimToPersistable(): PersistableSuspendedApp = + PersistableSuspendedApp(userId, packageName) + + /** + * Fill all fields. + */ + @SystemApi + fun fillData(pm: PackageManager, ignoreLargeData: Boolean) { + val info = pm.getApplicationInfoAsUser(packageName, + 0, + userId) + label = if (ignoreLargeData) null + else pm.getApplicationLabel(info) + uid = info.uid + if (ignoreLargeData) { + icon = null + } else { + XLog.d("Loading icon for ${info.packageName}") + val res = pm.getResourcesForApplicationAsUser(info.packageName, userId) + icon = BitmapFactory.decodeResource(res, info.icon) + } + } + + /** + * Remove unnecessary data for in-app transaction + */ + fun trimData() { + label = null + uid = -1 + icon = null + } + + companion object { + fun of(packageName: String): TransferableSuspendedApp { + return of(packageName, Process.myUserHandle().hashCode()) + } + + fun of(packageName: String, userId: Int): TransferableSuspendedApp { + return PersistableSuspendedApp(userId, packageName).copyToSimpleTransferableInfo() + } + + @JvmField + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): TransferableSuspendedApp { + return TransferableSuspendedApp(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeInt(userId) + parcel.writeString(packageName) + TextUtils.writeToParcel(label, parcel, flags) + parcel.writeInt(uid) + parcel.writeParcelable(icon, 0) + } + + override fun describeContents(): Int { + return 0 + } + + fun essentiallyEqual(that: TransferableSuspendedApp): Boolean = that.userId == this.userId && + that.packageName == this.packageName +} \ No newline at end of file diff --git a/app/src/main/java/moe/yuuta/workmode/utils/Utils.kt b/app/src/main/java/moe/yuuta/workmode/utils/Utils.kt index 2ac68e9..75a98e9 100644 --- a/app/src/main/java/moe/yuuta/workmode/utils/Utils.kt +++ b/app/src/main/java/moe/yuuta/workmode/utils/Utils.kt @@ -1,8 +1,12 @@ package moe.yuuta.workmode.utils +import android.annotation.SystemApi import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.GET_DISABLED_COMPONENTS +import android.content.pm.ResolveInfo import android.os.Bundle import android.os.Process import android.view.ViewGroup @@ -10,6 +14,8 @@ import android.widget.LinearLayout import androidx.core.view.children import com.google.android.material.tabs.TabLayout import moe.yuuta.workmode.BuildConfig +import moe.yuuta.workmode.suspend.data.PersistableSuspendedApp +import moe.yuuta.workmode.suspend.data.TransferableSuspendedApp import java.util.function.Predicate import java.util.stream.Collectors @@ -90,6 +96,76 @@ object Utils { } } + fun buildGeneralSuspendedAppInfoFilter(context: Context): Predicate { + val i = Intent(Intent.ACTION_MAIN) + i.addCategory(Intent.CATEGORY_HOME) + val launchers = context.packageManager.queryIntentActivitiesAsUser(i, 0, context.userId) + .stream() + .map { + return@map it.resolvePackageName + } + .collect(Collectors.toList()) + return object : Predicate { + override fun test(it: TransferableSuspendedApp): Boolean { + for (pkg in WHITELIST_PKGS) + if (pkg == it.packageName) { + return true + } + for (pkg in PROTECTED_PACKAGES) + if (pkg == it.packageName) { + return false + } + for (pkg in PROTECTED_PACKAGES_WIDE_MATCH) + if (it.packageName.startsWith(pkg)) { + return false + } + val itUid = context.packageManager.getPackageUidAsUser(it.packageName, + GET_DISABLED_COMPONENTS, + it.userId) + for (uid in PROTECTED_UIDS) + if (uid == itUid) { + return false + } + if (launchers.contains(it.packageName)) { + return false + } + if (itUid < Process.FIRST_APPLICATION_UID) { + return false + } + return getLaunchIntentForPackageAsUser(it.packageName, context.packageManager, it.userId) != null + } + } + } + + fun getLaunchIntentForPackageAsUser(packageName: String, pm: PackageManager, userId: Int): Intent? { + // First see if the package has an INFO activity; the existence of + // such an activity is implied to be the desired front-door for the + // overall package (such as if it has multiple launcher entries). + val intentToResolve = Intent(Intent.ACTION_MAIN) + intentToResolve.addCategory(Intent.CATEGORY_INFO) + intentToResolve.setPackage(packageName) + var ris: List? = pm.queryIntentActivitiesAsUser(intentToResolve, 0, userId) + + // Otherwise, try to find a main launcher activity. + if (ris == null || ris.size <= 0) { + // reuse the intent instance + intentToResolve.removeCategory(Intent.CATEGORY_INFO) + intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER) + intentToResolve.setPackage(packageName) + ris = pm.queryIntentActivitiesAsUser(intentToResolve, 0, userId) + } + if (ris == null || ris.size <= 0) { + return null + } + val intent = Intent(intentToResolve) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.setClassName( + ris[0].activityInfo.packageName, + ris[0].activityInfo.name + ) + return intent + } + fun dumpExtras(bundle: Bundle?): String { val builder = StringBuilder() if (bundle != null) { @@ -124,4 +200,32 @@ object Utils { tabStrip.getChildAt(i).setOnTouchListener { v, event -> !enable } } } + + fun canSafelyLoadAppInfo(packageInfo: TransferableSuspendedApp, context: Context): Boolean { + return Utils.canSafelyLoadAppInfo(packageInfo, Process.myUserHandle().hashCode(), context) + } + + fun canSafelyLoadAppInfo(packageInfo: PersistableSuspendedApp, context: Context): Boolean { + return packageInfo.userId == Process.myUserHandle().hashCode() || + isAppInstalledInCurrentUser(packageInfo.packageName, context) + } + + fun canSafelyLoadAppInfo(packageInfo: TransferableSuspendedApp, userId: Int, context: Context): Boolean { + return packageInfo.userId == userId || + isAppInstalledInUser(packageInfo.packageName, context, userId) + } + + fun isAppInstalledInCurrentUser(packageName: String, context: Context): Boolean { + return isAppInstalledInUser(packageName, context, Process.myUserHandle().hashCode()) + } + + @SystemApi + fun isAppInstalledInUser(packageName: String, context: Context, userId: Int): Boolean { + try { + context.packageManager.getPackageInfoAsUser(packageName, PackageManager.GET_DISABLED_COMPONENTS, userId) + return true + } catch (e: PackageManager.NameNotFoundException) { + return false + } + } } \ No newline at end of file diff --git a/app/src/main/res/layout/item_application.xml b/app/src/main/res/layout/item_application.xml index 7ab17dc..3cceb66 100644 --- a/app/src/main/res/layout/item_application.xml +++ b/app/src/main/res/layout/item_application.xml @@ -4,11 +4,11 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="56dp"> + android:layout_height="72dp"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_application_select.xml b/app/src/main/res/layout/item_application_select.xml index c5068d4..c82ed84 100644 --- a/app/src/main/res/layout/item_application_select.xml +++ b/app/src/main/res/layout/item_application_select.xml @@ -4,11 +4,11 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="56dp"> + android:layout_height="72dp"> + + 小雅 设计 ]]> + 用户 %d \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8f7ca34..af3dd46 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,7 @@ 小雅 ]]> + User %d Y29tLmFuZHJvaWQudmVuZGluZw== -- cgit v1.2.3