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; package com.topjohnwu.magisk.utils;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@ -10,20 +13,33 @@ import com.topjohnwu.magisk.FileProvider;
import java.io.File; import java.io.File;
public class APKInstall { 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) { public static void install(Context c, File apk) {
c.startActivity(installIntent(c, apk)); c.startActivity(installIntent(c, apk));
} }
public static Intent installIntent(Context c, File apk) { public static void installAndWait(Activity c, File apk, BroadcastReceiver r) {
Intent install = new Intent(Intent.ACTION_INSTALL_PACKAGE); IntentFilter filter = new IntentFilter();
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); filter.addAction(Intent.ACTION_PACKAGE_ADDED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { filter.addDataScheme("package");
install.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk)); c.getApplicationContext().registerReceiver(r, filter);
} else {
apk.setReadable(true, false); Intent intent = installIntent(c, apk);
install.setData(Uri.fromFile(apk)); intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
} c.startActivityForResult(intent, 0);
return install;
} }
} }

View File

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

View File

@ -1,6 +1,7 @@
package com.topjohnwu.magisk.core.tasks 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.Context
import android.content.Intent import android.content.Intent
import android.widget.Toast 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.data.repository.NetworkService
import com.topjohnwu.magisk.ktx.inject import com.topjohnwu.magisk.ktx.inject
import com.topjohnwu.magisk.ktx.writeTo import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.signing.JarMap import com.topjohnwu.signing.JarMap
import com.topjohnwu.signing.SignApk import com.topjohnwu.signing.SignApk
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.lang.ref.WeakReference
import java.security.SecureRandom import java.security.SecureRandom
object HideAPK { object HideAPK {
@ -92,8 +92,39 @@ object HideAPK {
return true return true
} }
private suspend fun patchAndHide(context: Context, label: String): Boolean { private class WaitPackageReceiver(
val stub = File(context.cacheDir, "stub.apk") 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 { try {
svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub) svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub)
} catch (e: IOException) { } catch (e: IOException) {
@ -102,71 +133,29 @@ object HideAPK {
} }
// Generate a new random package name and signature // 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() val pkg = genPackageName()
Config.keyStoreRaw = "" Config.keyStoreRaw = ""
if (!patch(context, stub, repack, pkg, label)) if (!patch(activity, stub, repack, pkg, label))
return false return false
// Install the application // Install and auto launch app
if (!Shell.su("adb_pm_install $repack").exec().isSuccess) APKInstall.installAndWait(activity, repack, WaitPackageReceiver(pkg, activity))
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)
}
return true return true
} }
@Suppress("DEPRECATION") suspend fun hide(activity: Activity, label: String) {
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) { val result = withContext(Dispatchers.IO) {
patchAndHide(context, label) patchAndHide(activity, label)
} }
if (!result) { if (!result) {
Utils.toast(R.string.failure, Toast.LENGTH_LONG) Utils.toast(R.string.failure, Toast.LENGTH_LONG)
dialog.dismiss()
}
} }
} }
private fun restoreImpl(context: Context): Boolean { fun restore(activity: Activity) {
val apk = DynAPK.current(context) val apk = DynAPK.current(activity)
if (!Shell.su("adb_pm_install $apk").exec().isSuccess) APKInstall.installAndWait(activity, apk, WaitPackageReceiver(APPLICATION_ID, activity))
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()
}
}
} }
} }

View File

@ -6,10 +6,8 @@ import androidx.annotation.CallSuper
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.base.BaseActivity import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.ktx.coroutineScope
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -31,7 +29,6 @@ abstract class MarkDownDialog : DialogEvent(), KoinComponent {
applyView(view) applyView(view)
(ownerActivity as BaseActivity).lifecycleScope.launch { (ownerActivity as BaseActivity).lifecycleScope.launch {
val tv = view.findViewById<TextView>(R.id.md_txt) val tv = view.findViewById<TextView>(R.id.md_txt)
tv.coroutineScope = CoroutineScope(coroutineContext)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
markwon.setMarkdown(tv, getMarkdownText()) markwon.setMarkdown(tv, getMarkdownText())

View File

@ -21,7 +21,6 @@ import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.system.Os import android.system.Os
import android.text.PrecomputedText import android.text.PrecomputedText
@ -42,11 +41,13 @@ import androidx.core.widget.TextViewCompat
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.lifecycleScope
import androidx.transition.AutoTransition import androidx.transition.AutoTransition
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.AssetHack import com.topjohnwu.magisk.core.AssetHack
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.utils.currentLocale import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.utils.DynamicClassLoader import com.topjohnwu.magisk.utils.DynamicClassLoader
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
@ -249,15 +250,13 @@ fun Context.colorStateListCompat(@ColorRes id: Int) = try {
null 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 * Pass [start] and [end] dimensions, function will return left and right
* with respect to RTL layout direction * with respect to RTL layout direction
*/ */
fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> { fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
if (SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
) {
return end to start return end to start
} }
return start to end 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 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(value) = setTag(R.id.coroutineScope, value)
@set:BindingAdapter("precomputedText") @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.AddHomeIconEvent
import com.topjohnwu.magisk.events.RecreateEvent import com.topjohnwu.magisk.events.RecreateEvent
import com.topjohnwu.magisk.events.dialog.BiometricEvent import com.topjohnwu.magisk.events.dialog.BiometricEvent
import com.topjohnwu.magisk.ktx.activity
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -105,17 +106,19 @@ class SettingsViewModel(
is Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().publish() is Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().publish()
is ClearRepoCache -> clearRepoCache() is ClearRepoCache -> clearRepoCache()
is SystemlessHosts -> createHosts() is SystemlessHosts -> createHosts()
is Restore -> HideAPK.restore(view.context) is Restore -> HideAPK.restore(view.activity)
is AddShortcut -> AddHomeIconEvent().publish() is AddShortcut -> AddHomeIconEvent().publish()
else -> callback() else -> callback()
} }
override fun onItemChanged(view: View, item: BaseSettingsItem) = when (item) { override fun onItemChanged(view: View, item: BaseSettingsItem) {
when (item) {
is Language -> RecreateEvent().publish() is Language -> RecreateEvent().publish()
is UpdateChannel -> openUrlIfNecessary(view) is UpdateChannel -> openUrlIfNecessary(view)
is Hide -> HideAPK.hide(view.context, item.value) is Hide -> viewModelScope.launch { HideAPK.hide(view.activity, item.value) }
else -> Unit else -> Unit
} }
}
private fun openUrlIfNecessary(view: View) { private fun openUrlIfNecessary(view: View) {
UpdateChannelUrl.refresh() UpdateChannelUrl.refresh()