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 package com.topjohnwu.magisk.arch
import android.content.Context import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.content.Intent import android.annotation.SuppressLint
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContract import android.widget.Toast
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
import com.topjohnwu.magisk.R 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.core.tasks.HideAPK
import com.topjohnwu.magisk.di.ServiceLocator import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ui.theme.Theme import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import java.util.concurrent.CountDownLatch import kotlinx.coroutines.launch
abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() { abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
@ -25,9 +29,6 @@ abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<
private var doPreload = true private var doPreload = true
} }
private val latch = CountDownLatch(1)
private val uninstallPkg = registerForActivityResult(UninstallPackage) { latch.countDown() }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setTheme(Theme.selected.themeRes) setTheme(Theme.selected.themeRes)
@ -67,20 +68,30 @@ abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<
abstract fun showMainUI(savedInstanceState: Bundle?) abstract fun showMainUI(savedInstanceState: Bundle?)
private fun showInvalidStateMessage() { @SuppressLint("InlinedApi")
runOnUiThread { private fun showInvalidStateMessage(): Unit = runOnUiThread {
MagiskDialog(this).apply { MagiskDialog(this).apply {
setTitle(R.string.unsupport_nonroot_stub_title) setTitle(R.string.unsupport_nonroot_stub_title)
setMessage(R.string.unsupport_nonroot_stub_msg) setMessage(R.string.unsupport_nonroot_stub_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) { setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install text = R.string.install
onClick { HideAPK.restore(this@BaseMainActivity) } 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) setCancelable(false)
show() show()
} }
} }
}
private fun preLoad() { private fun preLoad() {
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG) val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
@ -107,23 +118,10 @@ abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<
Config.suManager = "" Config.suManager = ""
pkg ?: return pkg ?: return
if (!Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec().isSuccess) { if (!Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec().isSuccess) {
uninstallPkg.launch(pkg) // Uninstall through Android API
// Wait for the uninstallation to finish uninstallAndWait(pkg)
latch.await()
} }
} }
} }
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 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.annotation.CallSuper
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.databinding.Observable import androidx.databinding.Observable
@ -78,7 +80,7 @@ abstract class BaseViewModel(
} }
inline fun withExternalRW(crossinline callback: () -> Unit) { inline fun withExternalRW(crossinline callback: () -> Unit) {
withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) { withPermission(WRITE_EXTERNAL_STORAGE) {
if (!it) { if (!it) {
SnackbarEvent(R.string.external_rw_permission_denied).publish() SnackbarEvent(R.string.external_rw_permission_denied).publish()
} else { } 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 back() = BackPressEvent().publish()
fun <Event : ViewEvent> Event.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.core.view.WindowCompat
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.base.BaseActivity import com.topjohnwu.magisk.core.base.BaseActivity
@ -74,6 +75,13 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
binding.root.rootView.accessibilityDelegate = delegate 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() { override fun onResume() {
super.onResume() super.onResume()
viewModel.requestRefresh() viewModel.requestRefresh()

View File

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

View File

@ -21,7 +21,8 @@ import com.topjohnwu.magisk.signing.SignApk
import com.topjohnwu.magisk.utils.APKInstall import com.topjohnwu.magisk.utils.APKInstall
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.* import kotlinx.coroutines.Dispatchers
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
@ -151,9 +152,8 @@ object HideAPK {
} }
} }
@DelicateCoroutinesApi
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun restore(activity: Activity) { suspend fun restore(activity: Activity) {
val dialog = ProgressDialog(activity).apply { val dialog = ProgressDialog(activity).apply {
setTitle(activity.getString(R.string.restore_img_msg)) setTitle(activity.getString(R.string.restore_img_msg))
isIndeterminate = true isIndeterminate = true
@ -165,12 +165,12 @@ object HideAPK {
launchApp(activity, APPLICATION_ID) launchApp(activity, APPLICATION_ID)
dialog.dismiss() dialog.dismiss()
} }
GlobalScope.launch(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
session.install(activity, apk) session.install(activity, apk)
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
return@launch return@withContext
} }
session.waitIntent()?.let { activity.startActivity(it) } 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.net.Uri
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.annotation.StringRes
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.MainDirections import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.* import com.topjohnwu.magisk.arch.*
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.view.Shortcuts
class PermissionEvent( class PermissionEvent(
@ -67,7 +71,7 @@ class NavigationEvent(
) : ViewEvent(), ActivityExecutor { ) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) { override fun invoke(activity: UIActivity<*>) {
(activity as? NavigationActivity<*>)?.apply { (activity as? NavigationActivity<*>)?.apply {
if (pop) navigation?.popBackStack() if (pop) navigation.popBackStack()
directions.navigate() 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 onDeletePressed() = UninstallDialog().publish()
fun onManagerPressed() = when (state) { fun onManagerPressed() = when (state) {
State.LOADED -> withExternalRW { ManagerInstallDialog().publish() } State.LOADED -> withExternalRW {
withInstallPermission {
ManagerInstallDialog().publish()
}
}
State.LOADING -> SnackbarEvent(R.string.loading).publish() State.LOADING -> SnackbarEvent(R.string.loading).publish()
else -> SnackbarEvent(R.string.no_connection).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.di.AppContext
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.asText import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -105,6 +106,25 @@ object Hide : BaseSettingsItem.Input() {
object Restore : BaseSettingsItem.Blank() { object Restore : BaseSettingsItem.Blank() {
override val title = R.string.settings_restore_app_title.asText() override val title = R.string.settings_restore_app_title.asText()
override val description = R.string.settings_restore_app_summary.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() { 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.RecreateEvent
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.events.dialog.BiometricEvent import com.topjohnwu.magisk.events.dialog.BiometricEvent
import com.topjohnwu.magisk.events.dialog.RestoreAppDialog
import com.topjohnwu.magisk.ktx.activity 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
@ -103,7 +102,7 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().navigate() Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().navigate()
DenyListConfig -> SettingsFragmentDirections.actionSettingsFragmentToDenyFragment().navigate() DenyListConfig -> SettingsFragmentDirections.actionSettingsFragmentToDenyFragment().navigate()
SystemlessHosts -> createHosts() SystemlessHosts -> createHosts()
Restore -> RestoreAppDialog().publish() Hide, Restore -> withInstallPermission(andThen)
AddShortcut -> AddHomeIconEvent().publish() AddShortcut -> AddHomeIconEvent().publish()
else -> andThen() else -> andThen()
} }
@ -114,6 +113,7 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
Language -> RecreateEvent().publish() Language -> RecreateEvent().publish()
UpdateChannel -> openUrlIfNecessary(view) UpdateChannel -> openUrlIfNecessary(view)
is Hide -> viewModelScope.launch { HideAPK.hide(view.activity, item.value) } 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() Zygisk -> if (Zygisk.mismatch) SnackbarEvent(R.string.reboot_apply_change).publish()
else -> Unit 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_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="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="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_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="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> <string name="app_not_found">No app found to handle this action</string>