Check REQUEST_INSTALL_PACKAGES before actions

This commit is contained in:
topjohnwu 2022-02-14 02:15:50 -08:00
parent e36596470c
commit 5d400fbe90
14 changed files with 198 additions and 112 deletions

View File

@ -1,23 +1,27 @@
package com.topjohnwu.magisk.arch
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContract
import android.widget.Toast
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.JobService
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
import java.util.concurrent.CountDownLatch
import kotlinx.coroutines.launch
abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
@ -25,9 +29,6 @@ abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<
private var doPreload = true
}
private val latch = CountDownLatch(1)
private val uninstallPkg = registerForActivityResult(UninstallPackage) { latch.countDown() }
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(Theme.selected.themeRes)
@ -67,18 +68,28 @@ abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<
abstract fun showMainUI(savedInstanceState: Bundle?)
private fun showInvalidStateMessage() {
runOnUiThread {
MagiskDialog(this).apply {
setTitle(R.string.unsupport_nonroot_stub_title)
setMessage(R.string.unsupport_nonroot_stub_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install
onClick { HideAPK.restore(this@BaseMainActivity) }
@SuppressLint("InlinedApi")
private fun showInvalidStateMessage(): Unit = runOnUiThread {
MagiskDialog(this).apply {
setTitle(R.string.unsupport_nonroot_stub_title)
setMessage(R.string.unsupport_nonroot_stub_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install
onClick {
withPermission(REQUEST_INSTALL_PACKAGES) {
if (!it) {
Utils.toast(R.string.install_unknown_denied, Toast.LENGTH_SHORT)
showInvalidStateMessage()
} else {
lifecycleScope.launch {
HideAPK.restore(this@BaseMainActivity)
}
}
}
}
setCancelable(false)
show()
}
setCancelable(false)
show()
}
}
@ -107,23 +118,10 @@ abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<
Config.suManager = ""
pkg ?: return
if (!Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec().isSuccess) {
uninstallPkg.launch(pkg)
// Wait for the uninstallation to finish
latch.await()
// Uninstall through Android API
uninstallAndWait(pkg)
}
}
}
object UninstallPackage : ActivityResultContract<String, Boolean>() {
@Suppress("DEPRECATION")
override fun createIntent(context: Context, input: String): Intent {
val uri = Uri.Builder().scheme("package").opaquePart(input).build()
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri)
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?) = resultCode == RESULT_OK
}
}

View File

@ -1,6 +1,8 @@
package com.topjohnwu.magisk.arch
import android.Manifest
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.annotation.SuppressLint
import androidx.annotation.CallSuper
import androidx.databinding.Bindable
import androidx.databinding.Observable
@ -78,7 +80,7 @@ abstract class BaseViewModel(
}
inline fun withExternalRW(crossinline callback: () -> Unit) {
withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
withPermission(WRITE_EXTERNAL_STORAGE) {
if (!it) {
SnackbarEvent(R.string.external_rw_permission_denied).publish()
} else {
@ -87,6 +89,17 @@ abstract class BaseViewModel(
}
}
@SuppressLint("InlinedApi")
inline fun withInstallPermission(crossinline callback: () -> Unit) {
withPermission(REQUEST_INSTALL_PACKAGES) {
if (!it) {
SnackbarEvent(R.string.install_unknown_denied).publish()
} else {
callback()
}
}
}
fun back() = BackPressEvent().publish()
fun <Event : ViewEvent> Event.publish() {

View File

@ -10,6 +10,7 @@ import androidx.core.content.res.use
import androidx.core.view.WindowCompat
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.base.BaseActivity
@ -74,6 +75,13 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
binding.root.rootView.accessibilityDelegate = delegate
}
fun showSnackbar(
message: CharSequence,
length: Int = Snackbar.LENGTH_SHORT,
builder: Snackbar.() -> Unit = {}
) = Snackbar.make(snackbarView, message, length)
.setAnchorView(snackbarAnchorView).apply(builder).show()
override fun onResume() {
super.onResume()
viewModel.requestRefresh()

View File

@ -1,5 +1,6 @@
package com.topjohnwu.magisk.core.base
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.content.Context
import android.content.Intent
@ -9,11 +10,16 @@ import android.os.Build
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts.GetContent
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.utils.RequestInstall
import com.topjohnwu.magisk.core.utils.UninstallPackage
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.core.wrap
import com.topjohnwu.magisk.ktx.reflectField
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
abstract class BaseActivity : AppCompatActivity() {
@ -22,6 +28,10 @@ abstract class BaseActivity : AppCompatActivity() {
permissionCallback?.invoke(it)
permissionCallback = null
}
private val requestInstall = registerForActivityResult(RequestInstall()) {
permissionCallback?.invoke(it)
permissionCallback = null
}
private var contentCallback: ((Uri) -> Unit)? = null
private val getContent = registerForActivityResult(GetContent()) {
@ -29,6 +39,11 @@ abstract class BaseActivity : AppCompatActivity() {
contentCallback = null
}
private var uninstallLatch = CountDownLatch(1)
private val uninstallPkg = registerForActivityResult(UninstallPackage()) {
uninstallLatch.countDown()
}
override fun applyOverrideConfiguration(config: Configuration?) {
// Force applying our preferred local
config?.setLocale(currentLocale)
@ -57,7 +72,11 @@ abstract class BaseActivity : AppCompatActivity() {
return
}
permissionCallback = callback
requestPermission.launch(permission)
if (permission == REQUEST_INSTALL_PACKAGES) {
requestInstall.launch(Unit)
} else {
requestPermission.launch(permission)
}
}
fun getContent(type: String, callback: (Uri) -> Unit) {
@ -65,6 +84,13 @@ abstract class BaseActivity : AppCompatActivity() {
getContent.launch(type)
}
@WorkerThread
fun uninstallAndWait(pkg: String) {
uninstallLatch = CountDownLatch(1)
uninstallPkg.launch(pkg)
uninstallLatch.await(3, TimeUnit.SECONDS)
}
override fun recreate() {
startActivity(Intent().setComponent(intent.component))
finish()

View File

@ -21,7 +21,8 @@ import com.topjohnwu.magisk.signing.SignApk
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
@ -151,9 +152,8 @@ object HideAPK {
}
}
@DelicateCoroutinesApi
@Suppress("DEPRECATION")
fun restore(activity: Activity) {
suspend fun restore(activity: Activity) {
val dialog = ProgressDialog(activity).apply {
setTitle(activity.getString(R.string.restore_img_msg))
isIndeterminate = true
@ -165,12 +165,12 @@ object HideAPK {
launchApp(activity, APPLICATION_ID)
dialog.dismiss()
}
GlobalScope.launch(Dispatchers.IO) {
withContext(Dispatchers.IO) {
try {
session.install(activity, apk)
} catch (e: IOException) {
Timber.e(e)
return@launch
return@withContext
}
session.waitIntent()?.let { activity.startActivity(it) }
}

View File

@ -0,0 +1,34 @@
package com.topjohnwu.magisk.core.utils
import android.annotation.TargetApi
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContract
class RequestInstall : ActivityResultContract<Unit, Boolean>() {
@TargetApi(26)
override fun createIntent(context: Context, input: Unit): Intent {
// This will only be called on API 26+
return Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
.setData(Uri.parse("package:${context.packageName}"))
}
override fun parseResult(resultCode: Int, intent: Intent?) =
resultCode == Activity.RESULT_OK
override fun getSynchronousResult(
context: Context,
input: Unit
): SynchronousResult<Boolean>? {
if (Build.VERSION.SDK_INT < 26)
return SynchronousResult(true)
if (context.packageManager.canRequestPackageInstalls())
return SynchronousResult(true)
return null
}
}

View File

@ -0,0 +1,21 @@
package com.topjohnwu.magisk.core.utils
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract
class UninstallPackage : ActivityResultContract<String, Boolean>() {
@Suppress("DEPRECATION")
override fun createIntent(context: Context, input: String): Intent {
val uri = Uri.Builder().scheme("package").opaquePart(input).build()
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri)
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?) =
resultCode == Activity.RESULT_OK
}

View File

@ -1,44 +0,0 @@
package com.topjohnwu.magisk.events
import android.view.View
import androidx.annotation.StringRes
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.asText
class SnackbarEvent constructor(
private val msg: TextHolder,
private val length: Int = Snackbar.LENGTH_SHORT,
private val builder: Snackbar.() -> Unit = {}
) : ViewEvent(), ActivityExecutor {
constructor(
@StringRes res: Int,
length: Int = Snackbar.LENGTH_SHORT,
builder: Snackbar.() -> Unit = {}
) : this(res.asText(), length, builder)
constructor(
msg: String,
length: Int = Snackbar.LENGTH_SHORT,
builder: Snackbar.() -> Unit = {}
) : this(msg.asText(), length, builder)
private fun snackbar(
view: View,
anchor: View?,
message: String,
length: Int,
builder: Snackbar.() -> Unit
) = Snackbar.make(view, message, length).setAnchorView(anchor).apply(builder).show()
override fun invoke(activity: UIActivity<*>) {
snackbar(activity.snackbarView, activity.snackbarAnchorView,
msg.getText(activity.resources).toString(),
length, builder)
}
}

View File

@ -5,12 +5,16 @@ import android.content.Context
import android.net.Uri
import android.view.View
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.navigation.NavDirections
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.*
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.Shortcuts
class PermissionEvent(
@ -67,7 +71,7 @@ class NavigationEvent(
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
(activity as? NavigationActivity<*>)?.apply {
if (pop) navigation?.popBackStack()
if (pop) navigation.popBackStack()
directions.navigate()
}
}
@ -92,3 +96,26 @@ class SelectModuleEvent : ViewEvent(), FragmentExecutor {
}
}
}
class SnackbarEvent(
private val msg: TextHolder,
private val length: Int = Snackbar.LENGTH_SHORT,
private val builder: Snackbar.() -> Unit = {}
) : ViewEvent(), ActivityExecutor {
constructor(
@StringRes res: Int,
length: Int = Snackbar.LENGTH_SHORT,
builder: Snackbar.() -> Unit = {}
) : this(res.asText(), length, builder)
constructor(
msg: String,
length: Int = Snackbar.LENGTH_SHORT,
builder: Snackbar.() -> Unit = {}
) : this(msg.asText(), length, builder)
override fun invoke(activity: UIActivity<*>) {
activity.showSnackbar(msg.getText(activity.resources), length, builder)
}
}

View File

@ -1,22 +0,0 @@
package com.topjohnwu.magisk.events.dialog
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.view.MagiskDialog
class RestoreAppDialog : DialogEvent() {
override fun build(dialog: MagiskDialog) {
dialog.apply {
setTitle(R.string.settings_restore_app_title)
setMessage(R.string.restore_app_confirmation)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick { HideAPK.restore(dialog.ownerActivity!!) }
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
setCancelable(true)
}
}
}

View File

@ -123,7 +123,11 @@ class HomeViewModel(
fun onDeletePressed() = UninstallDialog().publish()
fun onManagerPressed() = when (state) {
State.LOADED -> withExternalRW { ManagerInstallDialog().publish() }
State.LOADED -> withExternalRW {
withInstallPermission {
ManagerInstallDialog().publish()
}
}
State.LOADING -> SnackbarEvent(R.string.loading).publish()
else -> SnackbarEvent(R.string.no_connection).publish()
}

View File

@ -25,6 +25,7 @@ import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -105,6 +106,25 @@ object Hide : BaseSettingsItem.Input() {
object Restore : BaseSettingsItem.Blank() {
override val title = R.string.settings_restore_app_title.asText()
override val description = R.string.settings_restore_app_summary.asText()
override fun onPressed(view: View, handler: Handler) {
handler.onItemPressed(view, this) {
MagiskDialog(view.context).apply {
setTitle(R.string.settings_restore_app_title)
setMessage(R.string.restore_app_confirmation)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
handler.onItemAction(view, this@Restore)
}
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
setCancelable(true)
}
}
}
}
object AddShortcut : BaseSettingsItem.Blank() {

View File

@ -20,7 +20,6 @@ import com.topjohnwu.magisk.events.AddHomeIconEvent
import com.topjohnwu.magisk.events.RecreateEvent
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.events.dialog.BiometricEvent
import com.topjohnwu.magisk.events.dialog.RestoreAppDialog
import com.topjohnwu.magisk.ktx.activity
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell
@ -103,7 +102,7 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().navigate()
DenyListConfig -> SettingsFragmentDirections.actionSettingsFragmentToDenyFragment().navigate()
SystemlessHosts -> createHosts()
Restore -> RestoreAppDialog().publish()
Hide, Restore -> withInstallPermission(andThen)
AddShortcut -> AddHomeIconEvent().publish()
else -> andThen()
}
@ -114,6 +113,7 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
Language -> RecreateEvent().publish()
UpdateChannel -> openUrlIfNecessary(view)
is Hide -> viewModelScope.launch { HideAPK.hide(view.activity, item.value) }
Restore -> viewModelScope.launch { HideAPK.restore(view.activity) }
Zygisk -> if (Zygisk.mismatch) SnackbarEvent(R.string.reboot_apply_change).publish()
else -> Unit
}

View File

@ -228,6 +228,7 @@
<string name="unsupport_nonroot_stub_msg">The hidden Magisk app cannot continue to work because root was lost. Please restore the original APK.</string>
<string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string>
<string name="external_rw_permission_denied">Grant storage permission to enable this functionality</string>
<string name="install_unknown_denied">Allow "install unknown apps" to enable this functionality</string>
<string name="add_shortcut_title">Add shortcut to home screen</string>
<string name="add_shortcut_msg">After hiding this app, its name and icon might become difficult to recognize. Do you want to add a pretty shortcut to the home screen?</string>
<string name="app_not_found">No app found to handle this action</string>