Use standard Android APIs for install and launch

This commit is contained in:
topjohnwu 2021-02-20 20:12:35 -08:00
parent ccb55205e6
commit 331b1f542f
6 changed files with 104 additions and 92 deletions

View File

@ -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);
}
}

View File

@ -84,11 +84,6 @@
android:name="androidx.room.MultiInstanceInvalidationService"
tools:node="remove" />
<!-- We don't use Device Credentials -->
<activity
android:name="androidx.biometric.DeviceCredentialHandlerActivity"
tools:node="remove" />
</application>
</manifest>

View File

@ -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))
}
}

View File

@ -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<TextView>(R.id.md_txt)
tv.coroutineScope = CoroutineScope(coroutineContext)
withContext(Dispatchers.IO) {
try {
markwon.setMarkdown(tv, getMarkdownText())

View File

@ -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<Int, Int> {
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")

View File

@ -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) {