Better stub hiding experience

This commit is contained in:
topjohnwu 2020-10-17 03:40:43 -07:00
parent aaaaa3d044
commit 2e4dc91b96
16 changed files with 162 additions and 134 deletions

View File

@ -1,5 +1,6 @@
package com.topjohnwu.magisk.core
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.util.Xml
@ -9,19 +10,16 @@ import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
import com.topjohnwu.magisk.core.magiskdb.StringDao
import com.topjohnwu.magisk.core.utils.BiometricHelper
import com.topjohnwu.magisk.core.utils.defaultLocale
import com.topjohnwu.magisk.core.utils.refreshLocale
import com.topjohnwu.magisk.data.preference.PreferenceModel
import com.topjohnwu.magisk.data.repository.DBConfig
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.ktx.inject
import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
import com.topjohnwu.superuser.io.SuFileInputStream
import org.xmlpull.v1.XmlPullParser
import java.io.File
import java.io.IOException
import java.io.InputStream
object Config : PreferenceModel, DBConfig {
@ -29,6 +27,15 @@ object Config : PreferenceModel, DBConfig {
override val settingsDao: SettingsDao by inject()
override val context: Context by inject(Protected)
@get:SuppressLint("ApplySharedPref")
val prefsFile: File get() {
// Flush prefs to disk
prefs.edit().apply {
remove(Key.ASKED_HOME)
}.commit()
return File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
}
object Key {
// db configs
const val ROOT_ACCESS = "root_access"
@ -119,7 +126,7 @@ object Config : PreferenceModel, DBConfig {
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
var suAutoResponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
@ -150,8 +157,12 @@ object Config : PreferenceModel, DBConfig {
private const val SU_FINGERPRINT = "su_fingerprint"
fun initialize() {
prefs.edit { parsePrefs() }
fun load(pkg: String) {
try {
context.contentResolver.openInputStream(Provider.PREFS_URI(pkg))?.use {
prefs.edit { parsePrefs(it) }
}
} catch (e: IOException) {}
prefs.edit {
// Settings migration
@ -173,10 +184,8 @@ object Config : PreferenceModel, DBConfig {
}
}
private fun SharedPreferences.Editor.parsePrefs() {
val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS)
if (config.exists()) runCatching {
val input = SuFileInputStream(config)
private fun SharedPreferences.Editor.parsePrefs(input: InputStream) {
runCatching {
val parser = Xml.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
parser.setInput(input, "UTF-8")
@ -220,21 +229,6 @@ object Config : PreferenceModel, DBConfig {
else -> parser.next()
}
}
config.delete()
}
}
fun export() {
// Flush prefs to disk
prefs.edit().apply {
remove(Key.ASKED_HOME)
}.commit()
val context = get<Context>(Protected)
val xml = File(
"${context.filesDir.parent}/shared_prefs",
"${context.packageName}_preferences.xml"
)
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
}
}

View File

@ -46,7 +46,6 @@ object Const {
}
object Url {
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
@ -58,11 +57,9 @@ object Const {
}
object Key {
// others
const val LINK_KEY = "Link"
const val ETAG_KEY = "ETag"
// intents
const val OPEN_SECTION = "section"
const val HIDDEN_PKG = "hidden_pkg"
}
object Value {

View File

@ -2,9 +2,13 @@ package com.topjohnwu.magisk.core
import android.content.Context
import android.content.pm.ProviderInfo
import android.net.Uri
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
import com.topjohnwu.magisk.FileProvider
import com.topjohnwu.magisk.core.su.SuCallbackHandler
import java.io.File
open class Provider : FileProvider() {
@ -16,4 +20,20 @@ open class Provider : FileProvider() {
SuCallbackHandler(context!!, method, extras)
return Bundle.EMPTY
}
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
return when (uri.encodedPath ?: return null) {
"/apk_file" -> ParcelFileDescriptor.open(File(context!!.packageCodePath), MODE_READ_ONLY)
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
else -> super.openFile(uri, mode)
}
}
companion object {
fun APK_URI(pkg: String) =
Uri.Builder().scheme("content").authority("$pkg.provider").path("apk_file").build()
fun PREFS_URI(pkg: String) =
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
}
}

View File

@ -3,9 +3,9 @@ package com.topjohnwu.magisk.core
import android.app.Activity
import android.content.Context
import android.os.Bundle
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.network.RawServices
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.view.Notifications
@ -29,18 +29,18 @@ open class SplashActivity : Activity() {
}
}
private fun handleRepackage() {
val pkg = Config.suManager
if (Config.suManager.isNotEmpty() && packageName == BuildConfig.APPLICATION_ID) {
Config.suManager = ""
Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
}
if (pkg == packageName) {
private fun handleRepackage(pkg: String?) {
if (packageName != APPLICATION_ID) {
runCatching {
// We are the manager, remove com.topjohnwu.magisk as it could be malware
packageManager.getApplicationInfo(BuildConfig.APPLICATION_ID, 0)
Shell.su("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec()
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
packageManager.getApplicationInfo(APPLICATION_ID, 0)
Shell.su("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
}
} else {
if (Config.suManager.isNotEmpty())
Config.suManager = ""
pkg ?: return
Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
}
}
@ -48,14 +48,16 @@ open class SplashActivity : Activity() {
// Pre-initialize root shell
Shell.getShell()
Config.initialize()
handleRepackage()
val hiddenPackage = intent.getStringExtra(Const.Key.HIDDEN_PKG)
Config.load(hiddenPackage ?: APPLICATION_ID)
handleRepackage(hiddenPackage)
Notifications.setup(this)
UpdateCheckService.schedule(this)
Shortcuts.setupDynamic(this)
// Pre-fetch network stuffs
get<RawServices>()
// Pre-fetch network services
get<NetworkService>()
DONE = true

View File

@ -69,8 +69,8 @@ abstract class BaseDownloader : BaseService(), KoinComponent {
// -- Download logic
private suspend fun Subject.startDownload() {
val skip = this is Subject.Magisk && file.checkSum("MD5", magisk.md5)
if (!skip) {
val skipDl = this is Subject.Magisk && file.checkSum("MD5", magisk.md5)
if (!skipDl) {
val stream = service.fetchFile(url).toProgressStream(this)
when (this) {
is Subject.Module -> // Download and process on-the-fly

View File

@ -5,20 +5,16 @@ import androidx.core.net.toFile
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action.APK.Restore
import com.topjohnwu.magisk.core.download.Action.APK.Upgrade
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.tasks.PatchAPK
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.ktx.relaunchApp
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.superuser.Shell
import java.io.File
private fun Context.patch(apk: File) {
val patched = File(apk.parent, "patched.apk")
PatchAPK.patch(this, apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
HideAPK.patch(this, apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
apk.delete()
patched.renameTo(apk)
}
@ -31,7 +27,7 @@ private fun BaseDownloader.notifyHide(id: Int) {
}
}
private suspend fun BaseDownloader.upgrade(subject: Subject.Manager) {
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager) {
val apk = subject.file.toFile()
val id = subject.notifyID()
if (isRunningAsStub) {
@ -53,20 +49,3 @@ private suspend fun BaseDownloader.upgrade(subject: Subject.Manager) {
patch(apk)
}
}
private fun BaseDownloader.restore(apk: File, id: Int) {
update(id) {
it.setProgress(0, 0, true)
.setProgress(0, 0, true)
.setContentTitle(getString(R.string.restore_img_msg))
.setContentText("")
}
Config.export()
Shell.su("pm install $apk && pm uninstall $packageName").exec()
}
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager) =
when (subject.action) {
is Upgrade -> upgrade(subject)
is Restore -> restore(subject.file.toFile(), subject.notifyID())
}

View File

@ -40,16 +40,12 @@ sealed class Subject : Parcelable {
@Parcelize
class Manager(
override val action: Action.APK,
private val app: ManagerJson = Info.remote.app,
val stub: StubJson = Info.remote.stub
) : Subject() {
override val title: String
get() = "MagiskManager-${app.version}(${app.versionCode})"
override val url: String
get() = app.link
override val action get() = Action.Download
override val title: String get() = "MagiskManager-${app.version}(${app.versionCode})"
override val url: String get() = app.link
@IgnoredOnParcel
override val file by lazy {

View File

@ -36,7 +36,7 @@ class SuRequestHandler(
if (policy.packageName == BuildConfig.APPLICATION_ID)
return false
when (Config.suAutoReponse) {
when (Config.suAutoResponse) {
Config.Value.SU_AUTO_DENY -> {
respond(SuPolicy.DENY, 0)
return false

View File

@ -1,20 +1,20 @@
package com.topjohnwu.magisk.core.tasks
import android.app.ProgressDialog
import android.content.Context
import android.content.Intent
import android.os.Build.VERSION.SDK_INT
import android.widget.Toast
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.core.utils.AXML
import com.topjohnwu.magisk.core.utils.Keygen
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.ktx.inject
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.signing.JarMap
import com.topjohnwu.signing.SignApk
import com.topjohnwu.superuser.Shell
@ -28,18 +28,20 @@ import java.io.FileOutputStream
import java.io.IOException
import java.security.SecureRandom
object PatchAPK {
object HideAPK {
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
private const val ALPHADOTS = "$ALPHA....."
private const val APP_ID = "com.topjohnwu.magisk"
private const val APP_NAME = "Magisk Manager"
// Some arbitrary limit
const val MAX_LABEL_LENGTH = 32
private fun genPackageName(): CharSequence {
private val svc: NetworkService by inject()
private val Context.APK_URI get() = Provider.APK_URI(packageName)
private val Context.PREFS_URI get() = Provider.PREFS_URI(packageName)
private fun genPackageName(): String {
val random = SecureRandom()
val len = 5 + random.nextInt(15)
val builder = StringBuilder(len)
@ -59,20 +61,20 @@ object PatchAPK {
val idx = random.nextInt(len - 1)
builder[idx] = '.'
}
return builder
return builder.toString()
}
fun patch(
context: Context,
apk: String, out: String,
pkg: CharSequence, label: CharSequence
pkg: String, label: CharSequence
): Boolean {
try {
val jar = JarMap.open(apk)
val je = jar.getJarEntry(Const.ANDROID_MANIFEST)
val xml = AXML(jar.getRawData(je))
if (!xml.findAndPatch(APP_ID to pkg.toString(), APP_NAME to label.toString()))
if (!xml.findAndPatch(APPLICATION_ID to pkg, APP_NAME to label.toString()))
return false
// Write apk changes
@ -91,7 +93,6 @@ object PatchAPK {
val dlStub = !isRunningAsStub && SDK_INT >= 28 && Const.Version.atLeast_20_2()
val src = if (dlStub) {
val stub = File(context.cacheDir, "stub.apk")
val svc = get<NetworkService>()
try {
svc.fetchFile(Info.remote.stub.link).byteStream().use {
it.writeTo(stub)
@ -117,23 +118,73 @@ object PatchAPK {
if (!Shell.su("adb_pm_install $repack").exec().isSuccess)
return false
Config.suManager = pkg.toString()
Config.export()
Shell.su("pm uninstall $APP_ID").submit()
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)
startActivity(intent)
}
return true
}
fun hideManager(context: Context, label: String) {
val progress = Notifications.progress(context, context.getString(R.string.hide_manager_title))
Notifications.mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build())
@Suppress("DEPRECATION")
fun hide(context: Context, label: String) {
val dialog = ProgressDialog.show(context, context.getString(R.string.hide_manager_title), "", true)
GlobalScope.launch {
val result = withContext(Dispatchers.IO) {
patchAndHide(context, label)
}
if (!result)
if (!result) {
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG)
Notifications.mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID)
dialog.dismiss()
}
}
}
private suspend fun downloadAndRestore(context: Context): Boolean {
val apk = if (isRunningAsStub) {
DynAPK.current(context)
} else {
File(context.cacheDir, "manager.apk").also { apk ->
try {
svc.fetchFile(Info.remote.app.link).byteStream().use {
it.writeTo(apk)
}
} catch (e: IOException) {
Timber.e(e)
return false
}
}
}
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.HIDDEN_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) {
downloadAndRestore(context)
}
if (!result) {
Utils.toast(R.string.restore_manager_fail_toast, Toast.LENGTH_LONG)
dialog.dismiss()
}
}
}
}

View File

@ -11,16 +11,14 @@ interface PreferenceModel {
val fileName: String
get() = "${context.packageName}_preferences"
val commitPrefs: Boolean
get() = false
val prefs: SharedPreferences
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
fun preferenceStrInt(
name: String,
default: Int,
writeDefault: Boolean = false,
commit: Boolean = commitPrefs
commit: Boolean = false
) = object: ReadWriteProperty<PreferenceModel, Int> {
val base = StringProperty(name, default.toString(), commit)
override fun getValue(thisRef: PreferenceModel, property: KProperty<*>): Int =
@ -33,37 +31,37 @@ interface PreferenceModel {
fun preference(
name: String,
default: Boolean,
commit: Boolean = commitPrefs
commit: Boolean = false
) = BooleanProperty(name, default, commit)
fun preference(
name: String,
default: Float,
commit: Boolean = commitPrefs
commit: Boolean = false
) = FloatProperty(name, default, commit)
fun preference(
name: String,
default: Int,
commit: Boolean = commitPrefs
commit: Boolean = false
) = IntProperty(name, default, commit)
fun preference(
name: String,
default: Long,
commit: Boolean = commitPrefs
commit: Boolean = false
) = LongProperty(name, default, commit)
fun preference(
name: String,
default: String,
commit: Boolean = commitPrefs
commit: Boolean = false
) = StringProperty(name, default, commit)
fun preference(
name: String,
default: Set<String>,
commit: Boolean = commitPrefs
commit: Boolean = false
) = StringSetProperty(name, default, commit)
}

View File

@ -2,7 +2,6 @@ package com.topjohnwu.magisk.events.dialog
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.ktx.res
@ -16,7 +15,7 @@ class ManagerInstallDialog : DialogEvent() {
override fun build(dialog: MagiskDialog) {
with(dialog) {
val subject = Subject.Manager(Action.APK.Upgrade)
val subject = Subject.Manager()
applyTitle(R.string.repo_install_title.res(R.string.app_name.res()))
applyMessage(R.string.repo_install_msg.res(subject.title))

View File

@ -11,7 +11,7 @@ import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.UpdateCheckService
import com.topjohnwu.magisk.core.tasks.PatchAPK
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.core.utils.BiometricHelper
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.availableLocales
@ -90,7 +90,7 @@ object Hide : BaseSettingsItem.Input() {
set(value) = set(value, field, { field = it }, BR.result, BR.error)
val maxLength
get() = PatchAPK.MAX_LABEL_LENGTH
get() = HideAPK.MAX_LABEL_LENGTH
@get:Bindable
val isError
@ -288,9 +288,9 @@ object AutomaticResponse : BaseSettingsItem.Selector() {
override val entryRes = R.array.auto_response
override val entryValRes = R.array.value_array
override var value = Config.suAutoReponse
override var value = Config.suAutoResponse
set(value) = setV(value, field, { field = it }) {
Config.suAutoReponse = entryValues[it].toInt()
Config.suAutoResponse = entryValues[it].toInt()
}
}

View File

@ -15,11 +15,8 @@ import com.topjohnwu.magisk.arch.diffListOf
import com.topjohnwu.magisk.arch.itemBindingOf
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.tasks.PatchAPK
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.events.AddHomeIconEvent
import com.topjohnwu.magisk.events.RecreateEvent
@ -104,7 +101,7 @@ class SettingsViewModel(
is Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().publish()
is ClearRepoCache -> clearRepoCache()
is SystemlessHosts -> createHosts()
is Restore -> restoreManager()
is Restore -> HideAPK.restore(view.context)
is AddShortcut -> AddHomeIconEvent().publish()
else -> callback()
}
@ -112,7 +109,7 @@ class SettingsViewModel(
override fun onItemChanged(view: View, item: BaseSettingsItem) = when (item) {
is Language -> RecreateEvent().publish()
is UpdateChannel -> openUrlIfNecessary(view)
is Hide -> PatchAPK.hideManager(view.context, item.value)
is Hide -> HideAPK.hide(view.context, item.value)
else -> Unit
}
@ -142,9 +139,4 @@ class SettingsViewModel(
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
}
}
private fun restoreManager() {
DownloadService.start(get(), Subject.Manager(Action.APK.Restore))
}
}

View File

@ -15,7 +15,6 @@ import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.core.SplashActivity
import com.topjohnwu.magisk.core.cmp
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.intent
@ -70,7 +69,7 @@ object Notifications {
}
fun managerUpdate(context: Context) {
val intent = DownloadService.pendingIntent(context, Subject.Manager(Action.APK.Upgrade))
val intent = DownloadService.pendingIntent(context, Subject.Manager())
val builder = updateBuilder(context)
.setContentTitle(context.getString(R.string.manager_update_title))

View File

@ -90,7 +90,7 @@ EOF
}
adb_pm_install() {
local tmp=/data/local/tmp/patched.apk
local tmp=/data/local/tmp/temp.apk
cp -f "$1" $tmp
chmod 644 $tmp
su 2000 -c pm install $tmp || pm install $tmp

View File

@ -217,7 +217,8 @@
<string name="done">Done!</string>
<string name="failure">Failed</string>
<string name="hide_manager_title">Hiding Magisk Manager…</string>
<string name="hide_manager_fail_toast">Hide Magisk Manager failed.</string>
<string name="hide_manager_fail_toast">Hide Magisk Manager failed</string>
<string name="restore_manager_fail_toast">Restoring Magisk Manager failed</string>
<string name="open_link_failed_toast">No application found to open the link</string>
<string name="complete_uninstall">Complete Uninstall</string>
<string name="restore_img">Restore Images</string>