From 331b1f542f0f70eca50adf1868d4a01906836969 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Sat, 20 Feb 2021 20:12:35 -0800 Subject: [PATCH] Use standard Android APIs for install and launch --- .../topjohnwu/magisk/utils/APKInstall.java | 38 ++++-- app/src/main/AndroidManifest.xml | 5 - .../topjohnwu/magisk/core/tasks/HideAPK.kt | 111 ++++++++---------- .../magisk/events/dialog/MarkDownDialog.kt | 3 - .../java/com/topjohnwu/magisk/ktx/XAndroid.kt | 24 +++- .../magisk/ui/settings/SettingsViewModel.kt | 15 ++- 6 files changed, 104 insertions(+), 92 deletions(-) diff --git a/app/shared/src/main/java/com/topjohnwu/magisk/utils/APKInstall.java b/app/shared/src/main/java/com/topjohnwu/magisk/utils/APKInstall.java index 2270017ec..3efe27d9e 100644 --- a/app/shared/src/main/java/com/topjohnwu/magisk/utils/APKInstall.java +++ b/app/shared/src/main/java/com/topjohnwu/magisk/utils/APKInstall.java @@ -1,7 +1,10 @@ package com.topjohnwu.magisk.utils; +import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.net.Uri; import android.os.Build; @@ -10,20 +13,33 @@ import com.topjohnwu.magisk.FileProvider; import java.io.File; public class APKInstall { + + public static Intent installIntent(Context c, File apk) { + Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (Build.VERSION.SDK_INT >= 24) { + intent.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk)); + } else { + apk.setReadable(true, false); + intent.setData(Uri.fromFile(apk)); + } + return intent; + } + public static void install(Context c, File apk) { c.startActivity(installIntent(c, apk)); } - public static Intent installIntent(Context c, File apk) { - Intent install = new Intent(Intent.ACTION_INSTALL_PACKAGE); - install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - install.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk)); - } else { - apk.setReadable(true, false); - install.setData(Uri.fromFile(apk)); - } - return install; + public static void installAndWait(Activity c, File apk, BroadcastReceiver r) { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_REPLACED); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addDataScheme("package"); + c.getApplicationContext().registerReceiver(r, filter); + + Intent intent = installIntent(c, apk); + intent.putExtra(Intent.EXTRA_RETURN_RESULT, true); + c.startActivityForResult(intent, 0); } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 45e880079..4854eb8db 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -84,11 +84,6 @@ android:name="androidx.room.MultiInstanceInvalidationService" tools:node="remove" /> - - - diff --git a/app/src/main/java/com/topjohnwu/magisk/core/tasks/HideAPK.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/HideAPK.kt index dba447f39..bccf2ec47 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/tasks/HideAPK.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/HideAPK.kt @@ -1,6 +1,7 @@ package com.topjohnwu.magisk.core.tasks -import android.app.ProgressDialog +import android.app.Activity +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.widget.Toast @@ -16,18 +17,17 @@ import com.topjohnwu.magisk.core.utils.Keygen import com.topjohnwu.magisk.data.repository.NetworkService import com.topjohnwu.magisk.ktx.inject import com.topjohnwu.magisk.ktx.writeTo +import com.topjohnwu.magisk.utils.APKInstall import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.signing.JarMap import com.topjohnwu.signing.SignApk -import com.topjohnwu.superuser.Shell import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File import java.io.FileOutputStream import java.io.IOException +import java.lang.ref.WeakReference import java.security.SecureRandom object HideAPK { @@ -92,8 +92,39 @@ object HideAPK { return true } - private suspend fun patchAndHide(context: Context, label: String): Boolean { - val stub = File(context.cacheDir, "stub.apk") + private class WaitPackageReceiver( + private val pkg: String, + activity: Activity + ) : BroadcastReceiver() { + + private val activity = WeakReference(activity) + + private fun launchApp(): Unit = activity.get()?.run { + val intent = packageManager.getLaunchIntentForPackage(pkg) ?: return + Config.suManager = if (pkg == APPLICATION_ID) "" else pkg + grantUriPermission(pkg, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION) + grantUriPermission(pkg, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.putExtra(Const.Key.PREV_PKG, packageName) + startActivity(intent) + finish() + } ?: Unit + + override fun onReceive(context: Context, intent: Intent) { + when (intent.action ?: return) { + Intent.ACTION_PACKAGE_REPLACED, Intent.ACTION_PACKAGE_ADDED -> { + val newPkg = intent.data?.encodedSchemeSpecificPart.orEmpty() + if (newPkg == pkg) { + context.unregisterReceiver(this) + launchApp() + } + } + } + } + + } + + private suspend fun patchAndHide(activity: Activity, label: String): Boolean { + val stub = File(activity.cacheDir, "stub.apk") try { svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub) } catch (e: IOException) { @@ -102,71 +133,29 @@ object HideAPK { } // Generate a new random package name and signature - val repack = File(context.cacheDir, "patched.apk") + val repack = File(activity.cacheDir, "patched.apk") val pkg = genPackageName() Config.keyStoreRaw = "" - if (!patch(context, stub, repack, pkg, label)) + if (!patch(activity, stub, repack, pkg, label)) return false - // Install the application - if (!Shell.su("adb_pm_install $repack").exec().isSuccess) - return false - - context.apply { - val intent = packageManager.getLaunchIntentForPackage(pkg) ?: return false - Config.suManager = pkg - grantUriPermission(pkg, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION) - grantUriPermission(pkg, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION) - intent.putExtra(Const.Key.PREV_PKG, packageName) - startActivity(intent) - } - + // Install and auto launch app + APKInstall.installAndWait(activity, repack, WaitPackageReceiver(pkg, activity)) return true } - @Suppress("DEPRECATION") - fun hide(context: Context, label: String) { - val dialog = ProgressDialog.show(context, context.getString(R.string.hide_app_title), "", true) - GlobalScope.launch { - val result = withContext(Dispatchers.IO) { - patchAndHide(context, label) - } - if (!result) { - Utils.toast(R.string.failure, Toast.LENGTH_LONG) - dialog.dismiss() - } + suspend fun hide(activity: Activity, label: String) { + val result = withContext(Dispatchers.IO) { + patchAndHide(activity, label) + } + if (!result) { + Utils.toast(R.string.failure, Toast.LENGTH_LONG) } } - private fun restoreImpl(context: Context): Boolean { - val apk = DynAPK.current(context) - if (!Shell.su("adb_pm_install $apk").exec().isSuccess) - return false - - context.apply { - val intent = packageManager.getLaunchIntentForPackage(APPLICATION_ID) ?: return false - Config.suManager = "" - grantUriPermission(APPLICATION_ID, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION) - grantUriPermission(APPLICATION_ID, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION) - intent.putExtra(Const.Key.PREV_PKG, packageName) - startActivity(intent) - } - - return true - } - - @Suppress("DEPRECATION") - fun restore(context: Context) { - val dialog = ProgressDialog.show(context, context.getString(R.string.restore_img_msg), "", true) - GlobalScope.launch { - val result = withContext(Dispatchers.IO) { - restoreImpl(context) - } - if (!result) { - Utils.toast(R.string.failure, Toast.LENGTH_LONG) - dialog.dismiss() - } - } + fun restore(activity: Activity) { + val apk = DynAPK.current(activity) + APKInstall.installAndWait(activity, apk, WaitPackageReceiver(APPLICATION_ID, activity)) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/events/dialog/MarkDownDialog.kt b/app/src/main/java/com/topjohnwu/magisk/events/dialog/MarkDownDialog.kt index 2891d3b39..56b95b40f 100644 --- a/app/src/main/java/com/topjohnwu/magisk/events/dialog/MarkDownDialog.kt +++ b/app/src/main/java/com/topjohnwu/magisk/events/dialog/MarkDownDialog.kt @@ -6,10 +6,8 @@ import androidx.annotation.CallSuper import androidx.lifecycle.lifecycleScope import com.topjohnwu.magisk.R import com.topjohnwu.magisk.core.base.BaseActivity -import com.topjohnwu.magisk.ktx.coroutineScope import com.topjohnwu.magisk.view.MagiskDialog import io.noties.markwon.Markwon -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -31,7 +29,6 @@ abstract class MarkDownDialog : DialogEvent(), KoinComponent { applyView(view) (ownerActivity as BaseActivity).lifecycleScope.launch { val tv = view.findViewById(R.id.md_txt) - tv.coroutineScope = CoroutineScope(coroutineContext) withContext(Dispatchers.IO) { try { markwon.setMarkdown(tv, getMarkdownText()) diff --git a/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt b/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt index f970649f9..040d07a53 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt @@ -21,7 +21,6 @@ import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.LayerDrawable import android.net.Uri -import android.os.Build import android.os.Build.VERSION.SDK_INT import android.system.Os import android.text.PrecomputedText @@ -42,11 +41,13 @@ import androidx.core.widget.TextViewCompat import androidx.databinding.BindingAdapter import androidx.fragment.app.Fragment import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import androidx.lifecycle.lifecycleScope import androidx.transition.AutoTransition import androidx.transition.TransitionManager import com.topjohnwu.magisk.R import com.topjohnwu.magisk.core.AssetHack import com.topjohnwu.magisk.core.Const +import com.topjohnwu.magisk.core.base.BaseActivity import com.topjohnwu.magisk.core.utils.currentLocale import com.topjohnwu.magisk.utils.DynamicClassLoader import com.topjohnwu.magisk.utils.Utils @@ -249,15 +250,13 @@ fun Context.colorStateListCompat(@ColorRes id: Int) = try { null } -fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.getDrawable(this, id) +fun Context.drawableCompat(@DrawableRes id: Int) = AppCompatResources.getDrawable(this, id) /** * Pass [start] and [end] dimensions, function will return left and right * with respect to RTL layout direction */ fun Context.startEndToLeftRight(start: Int, end: Int): Pair { - if (SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && - resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL - ) { + if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL) { return end to start } return start to end @@ -317,8 +316,21 @@ fun ViewGroup.startAnimations() { ) } +val View.activity: Activity get() { + var context = context + while(true) { + if (context !is ContextWrapper) + error("View is not attached to activity") + if (context is Activity) + return context + context = context.baseContext + } +} + var View.coroutineScope: CoroutineScope - get() = getTag(R.id.coroutineScope) as? CoroutineScope ?: GlobalScope + get() = getTag(R.id.coroutineScope) as? CoroutineScope + ?: (activity as? BaseActivity)?.lifecycleScope + ?: GlobalScope set(value) = setTag(R.id.coroutineScope, value) @set:BindingAdapter("precomputedText") diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt index 624eff3f0..805aa405f 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt @@ -21,6 +21,7 @@ import com.topjohnwu.magisk.data.database.RepoDao import com.topjohnwu.magisk.events.AddHomeIconEvent import com.topjohnwu.magisk.events.RecreateEvent import com.topjohnwu.magisk.events.dialog.BiometricEvent +import com.topjohnwu.magisk.ktx.activity import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.superuser.Shell import kotlinx.coroutines.launch @@ -105,16 +106,18 @@ class SettingsViewModel( is Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().publish() is ClearRepoCache -> clearRepoCache() is SystemlessHosts -> createHosts() - is Restore -> HideAPK.restore(view.context) + is Restore -> HideAPK.restore(view.activity) is AddShortcut -> AddHomeIconEvent().publish() else -> callback() } - override fun onItemChanged(view: View, item: BaseSettingsItem) = when (item) { - is Language -> RecreateEvent().publish() - is UpdateChannel -> openUrlIfNecessary(view) - is Hide -> HideAPK.hide(view.context, item.value) - else -> Unit + override fun onItemChanged(view: View, item: BaseSettingsItem) { + when (item) { + is Language -> RecreateEvent().publish() + is UpdateChannel -> openUrlIfNecessary(view) + is Hide -> viewModelScope.launch { HideAPK.hide(view.activity, item.value) } + else -> Unit + } } private fun openUrlIfNecessary(view: View) {