aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuutaW <17158086+trumeet@users.noreply.github.com>2019-03-30 16:19:07 -0700
committerYuutaW <17158086+Trumeet@users.noreply.github.com>2019-03-30 16:19:07 -0700
commit06fbdcac173aea88cb4d02c4806866c83e720307 (patch)
treedce2416bfd2d991c23cf79e7820c2cbadc1d59fb
parentb0d7fdf0cb31c54d47dcfbc5b39190ee39890bfa (diff)
downloadWorkMode-06fbdcac173aea88cb4d02c4806866c83e720307.tar
WorkMode-06fbdcac173aea88cb4d02c4806866c83e720307.tar.gz
WorkMode-06fbdcac173aea88cb4d02c4806866c83e720307.tar.bz2
WorkMode-06fbdcac173aea88cb4d02c4806866c83e720307.zip
feat(app/ci): implement multi user support
Signed-off-by: YuutaW <17158086+Trumeet@users.noreply.github.com>
-rw-r--r--.travis.yml3
-rw-r--r--app/src/debug/AndroidManifest.xml12
-rw-r--r--app/src/debug/java/moe/yuuta/workmode/debug/DebugActivity.kt139
-rw-r--r--app/src/main/AndroidManifest.xml4
-rw-r--r--app/src/main/aidl/moe/yuuta/workmode/IAccessor.aidl19
-rw-r--r--app/src/main/aidl/moe/yuuta/workmode/access/DumpResult.aidl4
-rw-r--r--app/src/main/aidl/moe/yuuta/workmode/suspend/data/TransferableSuspendedApp.aidl4
-rw-r--r--app/src/main/java/moe/yuuta/ext/HandlerThreadExecutor.java26
-rw-r--r--app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java22
-rw-r--r--app/src/main/java/moe/yuuta/gplicense/ServerManagedPolicy.java6
-rw-r--r--app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt139
-rw-r--r--app/src/main/java/moe/yuuta/workmode/MainActivity.kt116
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/AccessLayer.kt63
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/AccessLayerUtil.kt102
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/AccessorStarter.kt297
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/ApplicationAccessorStarter.kt9
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/DumpResult.kt29
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/HostInfo.kt25
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/ShellAccessorStarter.kt9
-rw-r--r--app/src/main/java/moe/yuuta/workmode/access/WorkModeAccessor.kt510
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/AsyncSuspender.kt60
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/SuspendTile.kt10
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/SuspendedApp.kt38
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/Suspender.kt40
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/data/PersistableSuspendedApp.kt41
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/data/SuspendedStorage.kt38
-rw-r--r--app/src/main/java/moe/yuuta/workmode/suspend/data/TransferableSuspendedApp.kt101
-rw-r--r--app/src/main/java/moe/yuuta/workmode/utils/Utils.kt104
-rw-r--r--app/src/main/res/layout/item_application.xml20
-rw-r--r--app/src/main/res/layout/item_application_select.xml20
-rw-r--r--app/src/main/res/values-zh-rCN/strings.xml1
-rw-r--r--app/src/main/res/values/strings.xml1
32 files changed, 1346 insertions, 666 deletions
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android">
+ <application tools:ignore="GoogleAppIndexingWarning">
+ <activity android:name="moe.yuuta.workmode.debug.DebugActivity"
+ android:label="Debugging" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest> \ 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">
+ <permission android:name="${applicationId}.ACCESS"
+ android:protectionLevel="signature" />
+ <uses-permission android:name="${applicationId}.ACCESS" />
+
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.CHECK_LICENSE" />
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<TransferableSuspendedApp> packages);
+ Bundle getSuspendedPackageAppExtras(in TransferableSuspendedApp packageInfo);
+ Bundle getSuspendedPackageLauncherExtras(in TransferableSuspendedApp packageInfo);
+ DumpResult dump(in TransferableSuspendedApp packageInfo);
+ Bundle setPackagesSuspended(in List<TransferableSuspendedApp> packages,
+ boolean suspended,
+ in PersistableBundle appExtras,
+ in PersistableBundle launcherExtras,
+ String dialogMessage);
+ Bundle apply(in Bundle dat, in TransferableSuspendedApp[] suspendList, int listMode, int status);
+ List<TransferableSuspendedApp> 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.
* <p>
* 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<List<TransferableSuspendedApp>>
private lateinit var mProgressBar: ProgressBar
private lateinit var fab: FloatingActionButton
- private fun setResultAndFinish(packageNames: Array<String>?) {
- setResult(if (packageNames == null) Activity.RESULT_CANCELED else Activity.RESULT_OK,
- Intent().putExtra(EXTRA_SELECTED_PACKAGE_NAME, packageNames ?: arrayOf()))
+ private fun setResultAndFinish(packages: List<TransferableSuspendedApp>?) {
+ val selected = arrayListOf<TransferableSuspendedApp>()
+ 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<List<SelectedApp>> {
- override fun run(): List<SelectedApp> {
- 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<List<SelectedApp>> {
- override fun onStart() {
- mProgressBar.visibility = View.VISIBLE
- fab.visibility = View.GONE
- }
-
- override fun onStop(success: Boolean, result: List<SelectedApp>?, 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<TransferableSuspendedApp>()
+ 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<SelectedApp>) {
@@ -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<String, Drawable> =
LruCache(Runtime.getRuntime().maxMemory().toInt() / 5)
private val mStoppableGroup: StoppableGroup = StoppableGroup()
- internal var data: List<SelectedApp> = listOf()
- internal val checked: MutableSet<String> = mutableSetOf()
+ internal var data: MutableList<SelectedApp> = 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<Unit>
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<Unit> {
- 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<TransferableSuspendedApp>(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGES)?.toSet() ?: setOf<TransferableSuspendedApp>()
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<List<String>> {
- override fun run(): List<String>? {
+ mSortDisplayStoppable = Async.beginTask(object : Runnable<List<PersistableSuspendedApp>> {
+ override fun run(): List<PersistableSuspendedApp>? {
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<List<String>> {
- override fun onStop(success: Boolean, result: List<String>?, e: Throwable?) {
+ }, object : Callback<List<PersistableSuspendedApp>> {
+ override fun onStop(success: Boolean, result: List<PersistableSuspendedApp>?, 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<TransferableSuspendedApp>()
+ 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<Adapter.VH>() {
private val mIconMemoryCaches: LruCache<String, Drawable> =
LruCache(Runtime.getRuntime().maxMemory().toInt() / 5)
private val mStoppableGroup: StoppableGroup = StoppableGroup()
- internal var data: List<String> = listOf()
+ internal var data: List<PersistableSuspendedApp> = 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<Adapter.VH>() {
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<String>, suspended: Boolean,
appExtras: PersistableBundle, launcherExtras: PersistableBundle,
- dialogMessage: String): Array<String> {
+ dialogMessage: String, userId: Int): Array<String> {
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<String>::class.java,
- Boolean::class.java,
- PersistableBundle::class.java,
- PersistableBundle::class.java,
- String::class.java,
- String::class.java,
- Int::class.java)
-
// It's an unstable design
val iPM: Field = mPM::class.java.getDeclaredField("mPM")
iPM.isAccessible = true
-
- return func.invoke(iPM.get(mPM),
- packageNames,
+ 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<String>
+ userId) as Array<String>
}
/**
* This method will SET your UID and you WON'T BE ABLE TO GO BACK.
* Create a new process and access it.
*/
- fun getSuspendedPackageAppExtras(packageName: String): PersistableBundle? {
+ 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<ApplicationInfo> =
+ 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<TransferableSuspendedApp>): Map<Int /* user id */, List<TransferableSuspendedApp>> {
+ // Collect them with specified user ids
+ val packagesWithUserIds = mutableMapOf<Int /* user id */, List<TransferableSuspendedApp>>()
+ packages.stream()
+ .forEach {
+ val list = packagesWithUserIds[it.userId]?.toMutableList() ?: mutableListOf()
+ list.add(it)
+ packagesWithUserIds[it.userId] = list
+ }
+ return packagesWithUserIds
+ }
+
+ fun suspend(packages: List<TransferableSuspendedApp>,
+ suspended: Boolean,
+ appExtras: BaseBundle,
+ launcherExtras: BaseBundle,
+ dialogMessage: String): Array<String> {
+ val packagesWithUserIds = collectUserIDs(packages)
+
+ val result = mutableListOf<String>()
+ 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<TransferableSuspendedApp>, suspended: Boolean): Array<String> =
+ 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<ApplicationInfo> =
+ accessLayer.getInstalledApplicationsAsUser(flags, userId)
+
+ fun getInstalledApplicationsAnyUser(flags: Int): List<ApplicationInfo> {
+ val userManager = accessLayer.mContext.getSystemService(UserManager::class.java)
+ val finalResult = mutableListOf<ApplicationInfo>()
+ 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<IAccessor>) {
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<String> {
- 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<String> {
+ 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 <T> start(context: Context, root: Boolean, thenRun: Function<AccessorStarter, T>): T {
+ var res: T? = null
+ var err: Throwable? = null
+ object : RootIPCReceiver<IAccessor>(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<out String>, 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<TransferableSuspendedApp>): Boolean {
+ return mService.isSuspended(packages)
}
- fun setPackagesSuspended(packageNames: Array<String>, suspended: Boolean,
+ fun dump(packageInfo: TransferableSuspendedApp): DumpResult =
+ mService.dump(packageInfo)
+
+ fun setPackagesSuspended(packages: List<TransferableSuspendedApp>, suspended: Boolean,
appExtras: PersistableBundle, launcherExtras: PersistableBundle,
- dialogMessage: String, root: Boolean): Array<String> {
- 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<String> {
+ 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<String> {
- 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<String>, 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<TransferableSuspendedApp>, 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<String, Int>()
+ .filter {
+ return@filter it.startsWith("c_")
+ }
+ .collect(Collectors.toList())
+ val map = hashMapOf<String, Int>()
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<TransferableSuspendedApp> =
+ mService.getInstalledApplicationsAcrossUser(flags)
+
+ fun release() {
+ mListener.release()
+ }
+
+ fun isConnected(): Boolean = mService.asBinder().isBinderAlive
+
+ fun suspend(packages: List<TransferableSuspendedApp>, suspended: Boolean): Array<String> =
+ 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<DumpResult> {
+ override fun createFromParcel(parcel: Parcel): DumpResult {
+ return DumpResult(parcel)
+ }
+
+ override fun newArray(size: Int): Array<DumpResult?> {
+ 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<out String>) {
- mContext = RootJava.getPackageContext(BuildConfig.APPLICATION_ID)
- pmAccess = AccessLayer(mContext)
- mContext.sendBroadcast(Intent(AccessorStarter.ACTION_UPDATE_UI_PROGRESS)
- .putExtra(AccessorStarter.EXTRA_SHOW_PROGRESS, true))
- var parcel = Parcel.obtain()
- val argsByteArray = ByteArraySerializer.deserialize(args[0])
- val argsParcel = Parcel.obtain()
- argsParcel.unmarshall(argsByteArray, 0, argsByteArray.size)
- argsParcel.setDataPosition(0)
- mLogPath = argsParcel.readString() ?: "/data/adb"
+ 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<String, Int>()
- 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<String, Int>
+ 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<TransferableSuspendedApp>): List<TransferableSuspendedApp> {
+ 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<TransferableSuspendedApp> {
+ val originalApplicationInfo = mutableMapOf<TransferableSuspendedApp, ApplicationInfo>()
+ 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<TransferableSuspendedApp>()
+ 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<TransferableSuspendedApp>): 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<String>, suspended: Boolean,
- appExtras: Bundle, launcherExtras: Bundle,
- dialogMessage: String): Array<String> =
- 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<String> =
- mContext.packageManager.getInstalledApplications(0)
- .stream()
- .filter(Utils.buildGeneralApplicationInfoFilter(mContext))
- .filter {
- return@filter pmAccess.isPackageSuspended(it.packageName) &&
- SuspendedApp.deserializeBundle(pmAccess.getSuspendedPackageLauncherExtras(it.packageName)).isSuspendedByWorkMode
- }
- .map {
- return@map it.packageName
- }
- .collect(Collectors.toList())
+ 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<TransferableSuspendedApp>,
+ 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<out TransferableSuspendedApp>, 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<String>, suspended: Boolean): Array<String> =
- pmAccess.setPackagesSuspended(packageNames,
- suspended,
- PersistableBundle(),
- SuspendedApp.getDefault().serializeBundle(), // We use LauncherExtras because they are easy to read
- mContext.getString(R.string.suspended_message))
+ override fun getInstalledApplicationsAcrossUser(flags: Int): MutableList<TransferableSuspendedApp> {
+ 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<String>, suspended: Boolean, callback: Callback<Array<String>>): Stoppable =
- Async.beginTask(object : Runnable<Array<String>> {
- override fun run(): Array<String> = Suspender(mContext).suspend(packageNames, suspended)
- }, callback)
-
- fun isSuspended(packageNames: Array<String>, callback: Callback<Boolean>): Stoppable =
- Async.beginTask(object : Runnable<Boolean> {
- override fun run(): Boolean = Suspender(mContext).isSuspended(packageNames)
- }, callback)
-
- fun applyFromSettings(callback: Callback<Unit>): Stoppable =
- Async.beginTask(object : Runnable<Unit> {
- override fun run(): Unit = Suspender(mContext).applyFromSettings()
- }, callback)
+ fun isSuspended(packages: List<TransferableSuspendedApp>): CompletableFuture<Boolean> = generalCall(Function {
+ return@Function it.isPackageSuspended(packages)
+ })
+
+ fun suspend(packages: List<TransferableSuspendedApp>, suspended: Boolean): CompletableFuture<Array<String>> = generalCall(
+ Function {
+ return@Function it.suspend(packages, suspended)
+ })
+
+ fun getInstalledApplicationsAcrossUser(flags: Int): CompletableFuture<List<TransferableSuspendedApp>> = generalCall(
+ Function {
+ return@Function it.getInstalledApplicationsAcrossUser(flags)
+ })
+
+ private fun <T> generalCall(thenApply: java.util.function.Function<AccessorStarter, T>): CompletableFuture<T> {
+ return CompletableFuture.supplyAsync(Supplier {
+ return@Supplier AccessorStarter.start(mContext, true, thenApply)
+ })
+ }
+
+ fun applyFromSettings(): CompletableFuture<Unit> = generalCall(Function { t -> t.apply() })
+
+ fun getSuspendedPackageAppExtras(packageInfo: TransferableSuspendedApp): CompletableFuture<Bundle?> = generalCall(Function {
+ return@Function it.getSuspendedPackageAppExtras(packageInfo)
+ })
+
+ fun getSuspendedPackageLauncherExtras(packageInfo: TransferableSuspendedApp): CompletableFuture<Bundle?> = generalCall(Function {
+ return@Function it.getSuspendedPackageLauncherExtras(packageInfo)
+ })
+
+ fun dump(packageInfo: TransferableSuspendedApp): CompletableFuture<DumpResult> = generalCall(Function {
+ return@Function it.dump(packageInfo)
+ })
+
+ private data class DataWrapper<T>(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<String>, suspended: Boolean): Array<String> =
- 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<String>): Boolean =
- ApplicationAccessorStarter(mContext).isPackageSuspended(packageNames, true)
-
- fun getPackagesSuspendedByWorkMode(): List<String> =
- 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<String> = (getStorage().getStringSet("list", setOf()) ?: listOf<String>()).toList()
+ fun getList(): List<PersistableSuspendedApp> =
+ _getList().stream()
+ .map {
+ return@map PersistableSuspendedApp(it)
+ }
+ .collect(Collectors.toList())
- fun setList(set: Set<String>) {
+ fun setList(set: Set<PersistableSuspendedApp>) {
+ _setList(set.stream()
+ .map {
+ return@map it.toString()
+ }
+ .collect(Collectors.toSet()))
+ }
+
+ /**
+ * Internal get list (string)
+ */
+ private fun _getList(): List<String> = (getStorage().getStringSet("list", setOf()) ?: listOf<String>()).toList()
+
+ private fun _setList(set: Set<String>) {
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>(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<TransferableSuspendedApp> {
+ override fun createFromParcel(parcel: Parcel): TransferableSuspendedApp {
+ return TransferableSuspendedApp(parcel)
+ }
+
+ override fun newArray(size: Int): Array<TransferableSuspendedApp?> {
+ 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<TransferableSuspendedApp> {
+ 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<TransferableSuspendedApp> {
+ 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<ResolveInfo>? = 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">
<ImageView
android:id="@android:id/icon"
- android:layout_width="48dp"
- android:layout_height="48dp"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="8dp"
@@ -23,9 +23,21 @@
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@android:id/icon"
android:layout_marginStart="16dp"
+ android:firstBaselineToTopHeight="32dp"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
android:textSize="16sp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
tools:text="App"/>
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toEndOf="@android:id/icon"
+ android:layout_marginStart="16dp"
+ android:firstBaselineToTopHeight="20dp"
+ app:layout_constraintTop_toBottomOf="@android:id/title"
+ android:textSize="12sp"
+ android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
+ tools:text="App"/>
</androidx.constraintlayout.widget.ConstraintLayout> \ 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">
<ImageView
android:id="@android:id/icon"
- android:layout_width="48dp"
- android:layout_height="48dp"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="8dp"
@@ -23,12 +23,24 @@
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@android:id/icon"
android:layout_marginStart="16dp"
+ android:firstBaselineToTopHeight="32dp"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
android:textSize="16sp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
tools:text="App"/>
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toEndOf="@android:id/icon"
+ android:layout_marginStart="16dp"
+ android:firstBaselineToTopHeight="20dp"
+ app:layout_constraintTop_toBottomOf="@android:id/title"
+ android:textSize="12sp"
+ android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
+ tools:text="App"/>
+
<CheckBox
android:id="@android:id/checkbox"
android:layout_width="wrap_content"
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index b2fb28d..b8959aa 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -30,4 +30,5 @@
<string name="about_icon_copyright"><![CDATA[
图标由 <a href="https://weibo.com/528556720">小雅</a> 设计
]]></string>
+ <string name="app_list_user_template">用户 %d</string>
</resources> \ 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 @@
<string name="about_icon_copyright"><![CDATA[
The icon is designed by <a href="https://twitter.com/xiaoya_er">小雅</a>
]]></string>
+ <string name="app_list_user_template">User %d</string>
<!-- #Anti-Crack -->
<string name="sys_id" translatable="false">Y29tLmFuZHJvaWQudmVuZGluZw==</string>