Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-15 18:08:00 +01:00
parent d327c31227
commit 0b2aca0925
638 changed files with 76461 additions and 0 deletions

3
app/core/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/build
src/debug
src/release

72
app/core/build.gradle.kts Normal file
View file

@ -0,0 +1,72 @@
plugins {
id("com.android.library")
kotlin("android")
kotlin("plugin.parcelize")
id("dev.zacsweers.moshix")
id("com.google.devtools.ksp")
}
setupCoreLib()
ksp {
arg("room.generateKotlin", "true")
}
android {
namespace = "com.topjohnwu.magisk.core"
defaultConfig {
buildConfigField("String", "APP_PACKAGE_NAME", "\"com.topjohnwu.magisk\"")
buildConfigField("int", "APP_VERSION_CODE", "${Config.versionCode}")
buildConfigField("String", "APP_VERSION_NAME", "\"${Config.version}\"")
buildConfigField("int", "STUB_VERSION", Config.stubVersion)
consumerProguardFile("proguard-rules.pro")
}
buildFeatures {
aidl = true
buildConfig = true
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
api(project(":shared"))
coreLibraryDesugaring(libs.jdk.libs)
api(libs.timber)
api(libs.markwon.core)
implementation(libs.bcpkix)
implementation(libs.commons.compress)
api(libs.libsu.core)
api(libs.libsu.service)
api(libs.libsu.nio)
implementation(libs.retrofit)
implementation(libs.retrofit.moshi)
implementation(libs.retrofit.scalars)
implementation(libs.okhttp)
implementation(libs.okhttp.logging)
implementation(libs.okhttp.dnsoverhttps)
implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)
implementation(libs.core.splashscreen)
implementation(libs.core.ktx)
implementation(libs.activity)
implementation(libs.collection.ktx)
implementation(libs.profileinstaller)
// We also implement all our tests in this module.
// However, we don't want to bundle test dependencies.
// That's why we make it compileOnly.
compileOnly(libs.test.junit)
compileOnly(libs.test.uiautomator)
}

41
app/core/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,41 @@
# Parcelable
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
# Kotlin
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static void check*(...);
public static void throw*(...);
}
-assumenosideeffects class java.util.Objects {
public static ** requireNonNull(...);
}
-assumenosideeffects public class kotlin.coroutines.jvm.internal.DebugMetadataKt {
private static ** getDebugMetadataAnnotation(...) return null;
}
# Stub
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
boolean mActivityHandlesConfigFlagsChecked;
int mActivityHandlesConfigFlags;
}
# Strip Timber verbose and debug logging
-assumenosideeffects class timber.log.Timber$Tree {
public void v(**);
public void d(**);
}
# With R8 full mode generic signatures are stripped for classes that are not
# kept. Suspend functions are wrapped in continuations where the type argument
# is used.
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
# Excessive obfuscation
-flattenpackagehierarchy
-allowaccessmodification
-dontwarn org.junit.**
-dontwarn org.apache.**

View file

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<permission
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
android:protectionLevel="signature"
tools:node="remove" />
<uses-permission
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
tools:node="remove" />
<application
android:name=".App"
android:icon="@drawable/ic_launcher"
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
tools:remove="android:appComponentFactory">
<receiver
android:name=".Receiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED" />
<action android:name="android.intent.action.UID_REMOVED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<service
android:name=".Service"
android:exported="false"
android:enabled="@bool/enable_fg_service"
android:foregroundServiceType="dataSync" />
<service
android:name=".JobService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
<provider
android:name=".Provider"
android:authorities="${applicationId}.provider"
android:directBootAware="true"
android:exported="false"
android:grantUriPermissions="true" />
<!-- We don't invalidate Room -->
<service
android:name="androidx.room.MultiInstanceInvalidationService"
tools:node="remove" />
<!-- We handle initialization ourselves -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
<!-- We handle profile installation ourselves -->
<receiver
android:name="androidx.profileinstaller.ProfileInstallReceiver"
tools:node="remove" />
</application>
</manifest>

View file

@ -0,0 +1,10 @@
// IRootUtils.aidl
package com.topjohnwu.magisk.core.utils;
// Declare any non-default types here with import statements
interface IRootUtils {
android.app.ActivityManager.RunningAppProcessInfo getAppProcess(int pid);
IBinder getFileSystem();
boolean addSystemlessHosts();
}

View file

@ -0,0 +1,27 @@
package com.topjohnwu.magisk.core
import android.app.Application
import android.content.Context
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.utils.RootUtils
open class App() : Application() {
constructor(o: Any) : this() {
val data = StubApk.Data(o)
// Add the root service name mapping
data.classToComponent[RootUtils::class.java.name] = data.rootService.name
// Send back the actual root service class
data.rootService = RootUtils::class.java
Info.stub = data
}
override fun attachBaseContext(context: Context) {
if (context is Application) {
AppContext.attachApplication(context)
} else {
super.attachBaseContext(context)
AppContext.attachApplication(this)
}
}
}

View file

@ -0,0 +1,131 @@
package com.topjohnwu.magisk.core
import android.app.Activity
import android.app.Application
import android.app.LocaleManager
import android.content.ComponentCallbacks2
import android.content.Context
import android.content.ContextWrapper
import android.content.res.Configuration
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.system.Os
import androidx.profileinstaller.ProfileInstaller
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.base.UntrackedActivity
import com.topjohnwu.magisk.core.utils.LocaleSetting
import com.topjohnwu.magisk.core.utils.NetworkObserver
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.core.utils.ShellInit
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.superuser.ipc.RootService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.launch
import timber.log.Timber
import java.lang.ref.WeakReference
import kotlin.system.exitProcess
lateinit var AppApkPath: String
private set
object AppContext : ContextWrapper(null),
Application.ActivityLifecycleCallbacks, ComponentCallbacks2 {
val foregroundActivity: Activity? get() = ref.get()
private var ref = WeakReference<Activity>(null)
private lateinit var application: Application
private lateinit var networkObserver: NetworkObserver
init {
// Always log full stack trace with Timber
Timber.plant(Timber.DebugTree())
Thread.setDefaultUncaughtExceptionHandler { _, e ->
Timber.e(e)
exitProcess(1)
}
Os.setenv("PATH", "${Os.getenv("PATH")}:/debug_ramdisk:/sbin", true)
}
override fun onConfigurationChanged(newConfig: Configuration) {
LocaleSetting.instance.updateResource(resources)
}
override fun onActivityStarted(activity: Activity) {
networkObserver.postCurrentState()
}
override fun onActivityResumed(activity: Activity) {
if (activity is UntrackedActivity) return
ref = WeakReference(activity)
}
override fun onActivityPaused(activity: Activity) {
if (activity is UntrackedActivity) return
ref.clear()
}
override fun getApplicationContext() = application
fun attachApplication(app: Application) {
application = app
val base = app.baseContext
attachBaseContext(base)
app.registerActivityLifecycleCallbacks(this)
app.registerComponentCallbacks(this)
AppApkPath = if (isRunningAsStub) {
StubApk.current(base).path
} else {
base.packageResourcePath
}
resources.patch()
val shellBuilder = Shell.Builder.create()
.setFlags(Shell.FLAG_MOUNT_MASTER)
.setInitializers(ShellInit::class.java)
.setContext(this)
.setTimeout(2)
Shell.setDefaultBuilder(shellBuilder)
Shell.EXECUTOR = Dispatchers.IO.asExecutor()
RootUtils.bindTask = RootService.bindOrTask(
intent<RootUtils>(),
UiThreadHandler.executor,
RootUtils.Connection
)
// Pre-heat the shell ASAP
Shell.getShell(null) {}
if (SDK_INT >= 34 && isRunningAsStub) {
// Send over the locale config manually
val lm = getSystemService(LocaleManager::class.java)
lm.overrideLocaleConfig = LocaleSetting.localeConfig
}
networkObserver = NetworkObserver.init(this)
if (!BuildConfig.DEBUG && !isRunningAsStub) {
GlobalScope.launch(Dispatchers.IO) {
ProfileInstaller.writeProfile(this@AppContext)
}
}
}
override fun createDeviceProtectedStorageContext(): Context {
return if (SDK_INT >= Build.VERSION_CODES.N) {
super.createDeviceProtectedStorageContext()
} else {
this
}
}
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
override fun onLowMemory() {}
override fun onTrimMemory(level: Int) {}
}

View file

@ -0,0 +1,209 @@
package com.topjohnwu.magisk.core
import android.os.Bundle
import androidx.core.content.edit
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.repository.DBConfig
import com.topjohnwu.magisk.core.repository.PreferenceConfig
import com.topjohnwu.magisk.core.utils.LocaleSetting
import kotlinx.coroutines.GlobalScope
object Config : PreferenceConfig, DBConfig {
override val stringDB get() = ServiceLocator.stringDB
override val settingsDB get() = ServiceLocator.settingsDB
override val context get() = ServiceLocator.deContext
override val coroutineScope get() = GlobalScope
object Key {
// db configs
const val ROOT_ACCESS = "root_access"
const val SU_MULTIUSER_MODE = "multiuser_mode"
const val SU_MNT_NS = "mnt_ns"
const val SU_BIOMETRIC = "su_biometric"
const val ZYGISK = "zygisk"
const val BOOTLOOP = "bootloop"
const val SU_MANAGER = "requester"
const val KEYSTORE = "keystore"
// prefs
const val SU_REQUEST_TIMEOUT = "su_request_timeout"
const val SU_AUTO_RESPONSE = "su_auto_response"
const val SU_NOTIFICATION = "su_notification"
const val SU_REAUTH = "su_reauth"
const val SU_TAPJACK = "su_tapjack"
const val SU_RESTRICT = "su_restrict"
const val CHECK_UPDATES = "check_update"
const val RELEASE_CHANNEL = "release_channel"
const val CUSTOM_CHANNEL = "custom_channel"
const val LOCALE = "locale"
const val DARK_THEME = "dark_theme_extended"
const val DOWNLOAD_DIR = "download_dir"
const val SAFETY = "safety_notice"
const val THEME_ORDINAL = "theme_ordinal"
const val ASKED_HOME = "asked_home"
const val DOH = "doh"
const val RAND_NAME = "rand_name"
val NO_MIGRATION = setOf(ASKED_HOME, SU_REQUEST_TIMEOUT,
SU_AUTO_RESPONSE, SU_REAUTH, SU_TAPJACK)
}
object OldValue {
// Update channels
const val DEFAULT_CHANNEL = -1
const val STABLE_CHANNEL = 0
const val BETA_CHANNEL = 1
const val CUSTOM_CHANNEL = 2
const val CANARY_CHANNEL = 3
const val DEBUG_CHANNEL = 4
}
object Value {
// Update channels
const val DEFAULT_CHANNEL = -1
const val STABLE_CHANNEL = 0
const val BETA_CHANNEL = 1
const val DEBUG_CHANNEL = 2
const val CUSTOM_CHANNEL = 3
// root access mode
const val ROOT_ACCESS_DISABLED = 0
const val ROOT_ACCESS_APPS_ONLY = 1
const val ROOT_ACCESS_ADB_ONLY = 2
const val ROOT_ACCESS_APPS_AND_ADB = 3
// su multiuser
const val MULTIUSER_MODE_OWNER_ONLY = 0
const val MULTIUSER_MODE_OWNER_MANAGED = 1
const val MULTIUSER_MODE_USER = 2
// su mnt ns
const val NAMESPACE_MODE_GLOBAL = 0
const val NAMESPACE_MODE_REQUESTER = 1
const val NAMESPACE_MODE_ISOLATE = 2
// su notification
const val NO_NOTIFICATION = 0
const val NOTIFICATION_TOAST = 1
// su auto response
const val SU_PROMPT = 0
const val SU_AUTO_DENY = 1
const val SU_AUTO_ALLOW = 2
// su timeout
val TIMEOUT_LIST = longArrayOf(0, -1, 10, 20, 30, 60)
}
@JvmField var keepVerity = false
@JvmField var keepEnc = false
@JvmField var recovery = false
var denyList = false
var askedHome by preference(Key.ASKED_HOME, false)
var bootloop by dbSettings(Key.BOOTLOOP, 0)
var safetyNotice by preference(Key.SAFETY, true)
var darkTheme by preference(Key.DARK_THEME, -1)
var themeOrdinal by preference(Key.THEME_ORDINAL, 0)
private var checkUpdatePrefs by preference(Key.CHECK_UPDATES, true)
private var localePrefs by preference(Key.LOCALE, "")
var doh by preference(Key.DOH, false)
var updateChannel by preference(Key.RELEASE_CHANNEL, Value.DEFAULT_CHANNEL)
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
var downloadDir by preference(Key.DOWNLOAD_DIR, "")
var randName by preference(Key.RAND_NAME, true)
var checkUpdate
get() = checkUpdatePrefs
set(value) {
if (checkUpdatePrefs != value) {
checkUpdatePrefs = value
JobService.schedule(AppContext)
}
}
var locale
get() = localePrefs
set(value) {
localePrefs = value
LocaleSetting.instance.setLocale(value)
}
var zygisk by dbSettings(Key.ZYGISK, Info.isEmulator)
var suManager by dbStrings(Key.SU_MANAGER, "", true)
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
var suAutoResponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
private var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
var suAuth
get() = Info.isDeviceSecure && suBiometric
set(value) {
suBiometric = value
}
var suReAuth by preference(Key.SU_REAUTH, false)
var suTapjack by preference(Key.SU_TAPJACK, true)
var suRestrict by preference(Key.SU_RESTRICT, false)
private const val SU_FINGERPRINT = "su_fingerprint"
private const val UPDATE_CHANNEL = "update_channel"
fun toBundle(): Bundle {
val map = prefs.all - Key.NO_MIGRATION
return Bundle().apply {
for ((key, value) in map) {
when (value) {
is String -> putString(key, value)
is Int -> putInt(key, value)
is Boolean -> putBoolean(key, value)
}
}
}
}
@Suppress("DEPRECATION")
private fun fromBundle(bundle: Bundle) {
val keys = bundle.keySet().apply { removeAll(Key.NO_MIGRATION) }
prefs.edit {
for (key in keys) {
when (val value = bundle.get(key)) {
is String -> putString(key, value)
is Int -> putInt(key, value)
is Boolean -> putBoolean(key, value)
}
}
}
}
fun init(bundle: Bundle?) {
// Only try to load prefs when fresh install
if (bundle != null && prefs.all.isEmpty()) {
fromBundle(bundle)
}
prefs.edit {
// Migrate su_fingerprint
if (prefs.getBoolean(SU_FINGERPRINT, false))
suBiometric = true
remove(SU_FINGERPRINT)
// Migrate update_channel
prefs.getString(UPDATE_CHANNEL, null)?.let {
val channel = when (it.toInt()) {
OldValue.STABLE_CHANNEL -> Value.STABLE_CHANNEL
OldValue.CANARY_CHANNEL, OldValue.BETA_CHANNEL -> Value.BETA_CHANNEL
OldValue.DEBUG_CHANNEL -> Value.DEBUG_CHANNEL
OldValue.CUSTOM_CHANNEL -> Value.CUSTOM_CHANNEL
else -> Value.DEFAULT_CHANNEL
}
putInt(Key.RELEASE_CHANNEL, channel)
}
remove(UPDATE_CHANNEL)
}
}
}

View file

@ -0,0 +1,70 @@
package com.topjohnwu.magisk.core
import android.os.Build
import android.os.Process
import com.topjohnwu.magisk.core.BuildConfig.APP_VERSION_CODE
@Suppress("DEPRECATION")
object Const {
val CPU_ABI: String get() = Build.SUPPORTED_ABIS[0]
// Null if 32-bit only or 64-bit only
val CPU_ABI_32 =
if (Build.SUPPORTED_64_BIT_ABIS.isEmpty()) null
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
// Paths
const val MODULE_PATH = "/data/adb/modules"
const val TMPDIR = "/dev/tmp"
const val MAGISK_LOG = "/cache/magisk.log"
// Misc
val USER_ID = Process.myUid() / 100000
object Version {
const val MIN_VERSION = "v22.0"
const val MIN_VERCODE = 22000
private fun isCanary() = (Info.env.versionCode % 100) != 0
fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary()
fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary()
fun atLeast_28_0() = Info.env.versionCode >= 28000 || isCanary()
fun atLeast_30_1() = Info.env.versionCode >= 30100 || isCanary()
}
object ID {
const val DOWNLOAD_JOB_ID = 6
const val CHECK_UPDATE_JOB_ID = 7
}
object Url {
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
const val GITHUB_API_URL = "https://api.github.com/"
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk-files/"
const val INVALID_URL = "https://example.com/"
}
object Key {
// intents
const val OPEN_SECTION = "section"
const val PREV_CONFIG = "prev_config"
}
object Value {
const val FLASH_ZIP = "flash"
const val PATCH_FILE = "patch"
const val FLASH_MAGISK = "magisk"
const val FLASH_INACTIVE_SLOT = "slot"
const val UNINSTALL = "uninstall"
}
object Nav {
const val HOME = "home"
const val SETTINGS = "settings"
const val MODULES = "modules"
const val SUPERUSER = "superuser"
}
}

View file

@ -0,0 +1,55 @@
@file:Suppress("DEPRECATION")
package com.topjohnwu.magisk.core
import android.content.ComponentName
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.ktx.unwrap
import com.topjohnwu.magisk.core.utils.LocaleSetting
fun Resources.addAssetPath(path: String) = StubApk.addAssetPath(this, path)
fun Resources.patch(): Resources {
if (isRunningAsStub)
addAssetPath(AppApkPath)
LocaleSetting.instance.updateResource(this)
return this
}
fun Context.patch(): Context {
unwrap().resources.patch()
return this
}
// Wrapping is only necessary for ContextThemeWrapper to support configuration overrides
fun Context.wrap(): Context {
patch()
return object : ContextWrapper(this) {
override fun createConfigurationContext(config: Configuration): Context {
return super.createConfigurationContext(config).wrap()
}
}
}
fun Class<*>.cmp(pkg: String) =
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
// Keep a reference to these resources to prevent it from
// being removed when running "remove unused resources"
val shouldKeepResources = listOf(
R.string.no_info_provided,
R.string.release_notes,
R.string.invalid_update_channel,
R.string.update_available,
R.string.app_changelog,
R.string.home_item_source,
R.drawable.ic_more,
R.array.allow_timeout,
)

View file

@ -0,0 +1,125 @@
package com.topjohnwu.magisk.core
import android.app.KeyguardManager
import android.os.Build
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.ktx.getProperty
import com.topjohnwu.magisk.core.model.UpdateInfo
import com.topjohnwu.magisk.core.repository.NetworkService
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils.fastCmd
import com.topjohnwu.superuser.ShellUtils.fastCmdResult
import kotlinx.coroutines.Runnable
val isRunningAsStub get() = Info.stub != null
object Info {
var stub: StubApk.Data? = null
private val EMPTY_UPDATE = UpdateInfo()
var update = EMPTY_UPDATE
private set
suspend fun fetchUpdate(svc: NetworkService): UpdateInfo? {
return if (update === EMPTY_UPDATE) {
svc.fetchUpdate()?.apply { update = this }
} else update
}
fun resetUpdate() {
update = EMPTY_UPDATE
}
var isRooted = false
var noDataExec = false
var patchBootVbmeta = false
@JvmStatic var env = Env()
private set
@JvmStatic var isSAR = false
private set
var legacySAR = false
private set
var isAB = false
private set
var slot = ""
private set
var isVendorBoot = false
private set
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
@JvmStatic val isFDE get() = crypto == "block"
@JvmStatic var ramdisk = false
private set
private var crypto = ""
val isEmulator =
Build.DEVICE.contains("vsoc")
|| getProperty("ro.kernel.qemu", "0") == "1"
|| getProperty("ro.boot.qemu", "0") == "1"
val isConnected = MutableLiveData(false)
val showSuperUser: Boolean get() {
return env.isActive && (Const.USER_ID == 0
|| Config.suMultiuserMode == Config.Value.MULTIUSER_MODE_USER)
}
val isDeviceSecure get() =
AppContext.getSystemService(KeyguardManager::class.java).isDeviceSecure
class Env(
val versionString: String = "",
val isDebug: Boolean = false,
code: Int = -1
) {
val versionCode = when {
code < Const.Version.MIN_VERCODE -> -1
isRooted -> code
else -> -1
}
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
val isActive = versionCode > 0
}
fun init(shell: Shell) {
if (shell.isRoot) {
val v = fastCmd(shell, "magisk -v").split(":")
env = Env(
v[0], v.size >= 3 && v[2] == "D",
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
)
Config.denyList = fastCmdResult(shell, "magisk --denylist status")
}
val map = mutableMapOf<String, String>()
val list = object : CallbackList<String>(Runnable::run) {
override fun onAddElement(e: String) {
val split = e.split("=")
if (split.size >= 2) {
map[split[0]] = split[1]
}
}
}
shell.newJob().add("(app_init)").to(list).exec()
fun getVar(name: String) = map[name] ?: ""
fun getBool(name: String) = map[name].toBoolean()
isSAR = getBool("SYSTEM_AS_ROOT")
ramdisk = getBool("RAMDISKEXIST")
isAB = getBool("ISAB")
patchBootVbmeta = getBool("PATCHVBMETAFLAG")
crypto = getVar("CRYPTOTYPE")
slot = getVar("SLOT")
legacySAR = getBool("LEGACYSAR")
isVendorBoot = getBool("VENDORBOOT")
// Default presets
Config.recovery = getBool("RECOVERYMODE")
Config.keepVerity = getBool("KEEPVERITY")
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
}
}

View file

@ -0,0 +1,103 @@
package com.topjohnwu.magisk.core
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Notification
import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobScheduler
import android.content.Context
import androidx.core.content.getSystemService
import com.topjohnwu.magisk.core.base.BaseJobService
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.core.download.DownloadSession
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.view.Notifications
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
class JobService : BaseJobService() {
private var mSession: Session? = null
@TargetApi(value = 34)
inner class Session(
private var params: JobParameters
) : DownloadSession {
override val context get() = this@JobService
val engine = DownloadEngine(this)
fun updateParams(params: JobParameters) {
this.params = params
engine.reattach()
}
override fun attachNotification(id: Int, builder: Notification.Builder) {
setNotification(params, id, builder.build(), JOB_END_NOTIFICATION_POLICY_REMOVE)
}
override fun onDownloadComplete() {
jobFinished(params, false)
}
}
@SuppressLint("NewApi")
override fun onStartJob(params: JobParameters): Boolean {
return when (params.jobId) {
Const.ID.CHECK_UPDATE_JOB_ID -> checkUpdate(params)
Const.ID.DOWNLOAD_JOB_ID -> downloadFile(params)
else -> false
}
}
override fun onStopJob(params: JobParameters?) = false
@TargetApi(value = 34)
private fun downloadFile(params: JobParameters): Boolean {
params.transientExtras.classLoader = Subject::class.java.classLoader
val subject = params.transientExtras
.getParcelable(DownloadEngine.SUBJECT_KEY, Subject::class.java) ?:
return false
val session = mSession?.also {
it.updateParams(params)
} ?: run {
Session(params).also { mSession = it }
}
session.engine.download(subject)
return true
}
private fun checkUpdate(params: JobParameters): Boolean {
GlobalScope.launch(Dispatchers.IO) {
Info.fetchUpdate(ServiceLocator.networkService)?.let {
if (Info.env.isActive && BuildConfig.APP_VERSION_CODE < it.versionCode)
Notifications.updateAvailable()
jobFinished(params, false)
}
}
return true
}
companion object {
fun schedule(context: Context) {
val scheduler = context.getSystemService<JobScheduler>() ?: return
if (Config.checkUpdate) {
val cmp = JobService::class.java.cmp(context.packageName)
val info = JobInfo.Builder(Const.ID.CHECK_UPDATE_JOB_ID, cmp)
.setPeriodic(TimeUnit.HOURS.toMillis(12))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setRequiresDeviceIdle(true)
.build()
scheduler.schedule(info)
} else {
scheduler.cancel(Const.ID.CHECK_UPDATE_JOB_ID)
}
}
}
}

View file

@ -0,0 +1,18 @@
package com.topjohnwu.magisk.core
import android.os.Bundle
import com.topjohnwu.magisk.core.base.BaseProvider
import com.topjohnwu.magisk.core.su.SuCallbackHandler
class Provider : BaseProvider() {
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
return when (method) {
SuCallbackHandler.LOG, SuCallbackHandler.NOTIFY -> {
SuCallbackHandler.run(context!!, method, extras)
Bundle.EMPTY
}
else -> Bundle.EMPTY
}
}
}

View file

@ -0,0 +1,68 @@
package com.topjohnwu.magisk.core
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import androidx.core.content.IntentCompat
import com.topjohnwu.magisk.core.base.BaseReceiver
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
open class Receiver : BaseReceiver() {
private val policyDB get() = ServiceLocator.policyDB
@SuppressLint("InlinedApi")
private fun getPkg(intent: Intent): String? {
val pkg = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)
return pkg ?: intent.data?.schemeSpecificPart
}
private fun getUid(intent: Intent): Int? {
val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
return if (uid == -1) null else uid
}
override fun onReceive(context: Context, intent: Intent?) {
intent ?: return
super.onReceive(context, intent)
fun rmPolicy(uid: Int) = GlobalScope.launch {
policyDB.delete(uid)
}
when (intent.action ?: return) {
DownloadEngine.ACTION -> {
IntentCompat.getParcelableExtra(
intent, DownloadEngine.SUBJECT_KEY, Subject::class.java)?.let {
DownloadEngine.start(context, it)
}
}
Intent.ACTION_PACKAGE_REPLACED -> {
// This will only work pre-O
if (Config.suReAuth)
getUid(intent)?.let { rmPolicy(it) }
}
Intent.ACTION_UID_REMOVED -> {
getUid(intent)?.let { rmPolicy(it) }
}
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
getPkg(intent)?.let { Shell.cmd("magisk --denylist rm $it").submit() }
}
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
Intent.ACTION_MY_PACKAGE_REPLACED -> {
@Suppress("DEPRECATION")
val installer = context.packageManager.getInstallerPackageName(context.packageName)
if (installer == context.packageName) {
Notifications.updateDone()
}
}
}
}
}

View file

@ -0,0 +1,39 @@
package com.topjohnwu.magisk.core
import android.app.Notification
import android.content.Intent
import android.os.Build
import androidx.core.app.ServiceCompat
import androidx.core.content.IntentCompat
import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.core.download.DownloadSession
import com.topjohnwu.magisk.core.download.Subject
class Service : BaseService(), DownloadSession {
private var mEngine: DownloadEngine? = null
override val context get() = this
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
if (intent.action == DownloadEngine.ACTION) {
IntentCompat
.getParcelableExtra(intent, DownloadEngine.SUBJECT_KEY, Subject::class.java)
?.let { subject ->
val engine = mEngine ?: DownloadEngine(this).also { mEngine = it }
engine.download(subject)
}
}
return START_NOT_STICKY
}
override fun attachNotification(id: Int, builder: Notification.Builder) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
startForeground(id, builder.build())
}
override fun onDownloadComplete() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
}
}

View file

@ -0,0 +1,139 @@
package com.topjohnwu.magisk.core.base
import android.Manifest.permission.POST_NOTIFICATIONS
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts.GetContent
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.ktx.reflectField
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.utils.RequestAuthentication
import com.topjohnwu.magisk.core.utils.RequestInstall
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
fun onActivityLaunch() {}
// Make the result type explicitly non-null
override fun onActivityResult(result: Uri)
}
interface UntrackedActivity
interface IActivityExtension {
val extension: ActivityExtension
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
extension.withPermission(permission, callback)
}
fun withAuthentication(callback: (Boolean) -> Unit) {
extension.withAuthentication(callback)
}
fun getContent(type: String, callback: ContentResultCallback) {
extension.getContent(type, callback)
}
}
class ActivityExtension(private val activity: ComponentActivity) {
private var permissionCallback: ((Boolean) -> Unit)? = null
private val requestPermission = activity.registerForActivityResult(RequestPermission()) {
permissionCallback?.invoke(it)
permissionCallback = null
}
private var installCallback: ((Boolean) -> Unit)? = null
private val requestInstall = activity.registerForActivityResult(RequestInstall()) {
installCallback?.invoke(it)
installCallback = null
}
private var authenticateCallback: ((Boolean) -> Unit)? = null
private val requestAuthenticate = activity.registerForActivityResult(RequestAuthentication()) {
authenticateCallback?.invoke(it)
authenticateCallback = null
}
private var contentCallback: ContentResultCallback? = null
private val getContent = activity.registerForActivityResult(GetContent()) {
if (it != null) contentCallback?.onActivityResult(it)
contentCallback = null
}
fun onCreate(savedInstanceState: Bundle?) {
contentCallback = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
} else {
savedInstanceState
?.getParcelable(CONTENT_CALLBACK_KEY, ContentResultCallback::class.java)
}
}
fun onSaveInstanceState(outState: Bundle) {
contentCallback?.let {
outState.putParcelable(CONTENT_CALLBACK_KEY, it)
}
}
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
permission == WRITE_EXTERNAL_STORAGE) {
// We do not need external rw on R+
callback(true)
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU &&
permission == POST_NOTIFICATIONS) {
// All apps have notification permissions before T
callback(true)
return
}
if (permission == REQUEST_INSTALL_PACKAGES) {
installCallback = callback
requestInstall.launch(Unit)
} else {
permissionCallback = callback
requestPermission.launch(permission)
}
}
fun withAuthentication(callback: (Boolean) -> Unit) {
authenticateCallback = callback
requestAuthenticate.launch(Unit)
}
fun getContent(type: String, callback: ContentResultCallback) {
contentCallback = callback
try {
getContent.launch(type)
callback.onActivityLaunch()
} catch (e: ActivityNotFoundException) {
activity.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
}
}
companion object {
private const val CONTENT_CALLBACK_KEY = "content_callback"
}
}
val Activity.launchPackage: String? get() {
return if (Build.VERSION.SDK_INT >= 34) {
launchedFromPackage
} else {
Activity::class.java.reflectField("mReferrer").get(this) as String?
}
}
fun Activity.relaunch() {
startActivity(Intent(intent).setFlags(0))
finish()
}

View file

@ -0,0 +1,11 @@
package com.topjohnwu.magisk.core.base
import android.app.job.JobService
import android.content.Context
import com.topjohnwu.magisk.core.patch
abstract class BaseJobService : JobService() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base.patch())
}
}

View file

@ -0,0 +1,21 @@
package com.topjohnwu.magisk.core.base
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.content.pm.ProviderInfo
import android.database.Cursor
import android.net.Uri
import com.topjohnwu.magisk.core.patch
open class BaseProvider : ContentProvider() {
override fun attachInfo(context: Context, info: ProviderInfo) {
super.attachInfo(context.patch(), info)
}
override fun onCreate() = true
override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
}

View file

@ -0,0 +1,14 @@
package com.topjohnwu.magisk.core.base
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.annotation.CallSuper
import com.topjohnwu.magisk.core.patch
abstract class BaseReceiver : BroadcastReceiver() {
@CallSuper
override fun onReceive(context: Context, intent: Intent?) {
context.patch()
}
}

View file

@ -0,0 +1,14 @@
package com.topjohnwu.magisk.core.base
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import com.topjohnwu.magisk.core.patch
open class BaseService : Service() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base.patch())
}
override fun onBind(intent: Intent?): IBinder? = null
}

View file

@ -0,0 +1,161 @@
package com.topjohnwu.magisk.core.base
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.BuildConfig.APP_PACKAGE_NAME
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.JobService
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import java.io.IOException
interface SplashScreenHost : IActivityExtension {
val splashController: SplashController<*>
fun onCreateUi(savedInstanceState: Bundle?)
fun showInvalidStateMessage()
}
class SplashController<T>(private val activity: T)
where T : ComponentActivity, T: SplashScreenHost {
companion object {
private var splashShown = false
}
private var shouldCreateUiOnResume = false
fun preOnCreate() {
if (isRunningAsStub && !splashShown) {
// Manually apply splash theme for stub
activity.theme.applyStyle(R.style.StubSplashTheme, true)
}
}
fun onCreate(savedInstanceState: Bundle?) {
if (!isRunningAsStub) {
val splashScreen = activity.installSplashScreen()
splashScreen.setKeepOnScreenCondition { !splashShown }
}
if (splashShown) {
doCreateUi(savedInstanceState)
} else {
Shell.getShell(Shell.EXECUTOR) {
if (isRunningAsStub && !it.isRoot) {
activity.showInvalidStateMessage()
return@getShell
}
activity.initializeApp()
activity.runOnUiThread {
splashShown = true
if (isRunningAsStub) {
// Re-launch main activity without splash theme
activity.relaunch()
} else {
if (activity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
doCreateUi(savedInstanceState)
} else {
shouldCreateUiOnResume = true
}
}
}
}
}
}
fun onResume() {
if (shouldCreateUiOnResume) {
doCreateUi(null)
}
}
private fun doCreateUi(savedInstanceState: Bundle?) {
shouldCreateUiOnResume = false
activity.onCreateUi(savedInstanceState)
}
private fun T.initializeApp() {
val prevPkg = launchPackage
val prevConfig = intent.getBundleExtra(Const.Key.PREV_CONFIG)
val isPackageMigration = prevPkg != null && prevConfig != null
Config.init(prevConfig)
if (packageName != APP_PACKAGE_NAME) {
runCatching {
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
packageManager.getApplicationInfo(APP_PACKAGE_NAME, 0)
Shell.cmd("(pm uninstall $APP_PACKAGE_NAME)& >/dev/null 2>&1").exec()
}
} else {
if (Config.suManager.isNotEmpty()) {
Config.suManager = ""
}
if (isPackageMigration) {
Shell.cmd("(pm uninstall $prevPkg)& >/dev/null 2>&1").exec()
}
}
if (isPackageMigration) {
runOnUiThread {
// Relaunch the process after package migration
StubApk.restartProcess(this)
}
return
}
// Validate stub APK
if (isRunningAsStub && (
// Version mismatch
Info.stub!!.version != BuildConfig.STUB_VERSION ||
// Not properly patched
intent.component!!.className.contains(AppMigration.PLACEHOLDER))
) {
withPermission(REQUEST_INSTALL_PACKAGES) { granted ->
if (granted) {
lifecycleScope.launch {
val apk = File(cacheDir, "stub.apk")
try {
assets.open("stub.apk").writeTo(apk)
AppMigration.upgradeStub(activity, apk)?.let {
startActivity(it)
}
} catch (e: IOException) {
Timber.e(e)
}
}
}
}
return
}
Notifications.setup()
JobService.schedule(this)
Shortcuts.setupDynamic(this)
// Pre-fetch network services
ServiceLocator.networkService
// Wait for root service
RootUtils.Connection.await()
}
}

View file

@ -0,0 +1,48 @@
package com.topjohnwu.magisk.core.data
import com.topjohnwu.magisk.core.model.ModuleJson
import com.topjohnwu.magisk.core.model.Release
import com.topjohnwu.magisk.core.model.UpdateJson
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.Streaming
import retrofit2.http.Url
interface RawUrl {
@GET
@Streaming
suspend fun fetchFile(@Url url: String): ResponseBody
@GET
suspend fun fetchString(@Url url: String): String
@GET
suspend fun fetchModuleJson(@Url url: String): ModuleJson
@GET
suspend fun fetchUpdateJson(@Url url: String): UpdateJson
}
interface GithubApiServices {
@GET("/repos/{owner}/{repo}/releases")
@Headers("Accept: application/vnd.github+json")
suspend fun fetchReleases(
@Path("owner") owner: String = "topjohnwu",
@Path("repo") repo: String = "Magisk",
@Query("per_page") per: Int = 10,
@Query("page") page: Int = 1,
): Response<MutableList<Release>>
@GET("/repos/{owner}/{repo}/releases/latest")
@Headers("Accept: application/vnd.github+json")
suspend fun fetchLatestRelease(
@Path("owner") owner: String = "topjohnwu",
@Path("repo") repo: String = "Magisk",
): Release
}

View file

@ -0,0 +1,53 @@
package com.topjohnwu.magisk.core.data
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Insert
import androidx.room.Query
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.topjohnwu.magisk.core.model.su.SuLog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.Calendar
@Database(version = 2, entities = [SuLog::class], exportSchema = false)
abstract class SuLogDatabase : RoomDatabase() {
abstract fun suLogDao(): SuLogDao
companion object {
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) = with(db) {
execSQL("ALTER TABLE logs ADD COLUMN target INTEGER NOT NULL DEFAULT -1")
execSQL("ALTER TABLE logs ADD COLUMN context TEXT NOT NULL DEFAULT ''")
execSQL("ALTER TABLE logs ADD COLUMN gids TEXT NOT NULL DEFAULT ''")
}
}
}
}
@Dao
abstract class SuLogDao(private val db: SuLogDatabase) {
private val twoWeeksAgo =
Calendar.getInstance().apply { add(Calendar.WEEK_OF_YEAR, -2) }.timeInMillis
suspend fun deleteAll() = withContext(Dispatchers.IO) { db.clearAllTables() }
suspend fun fetchAll(): MutableList<SuLog> {
deleteOutdated()
return fetch()
}
@Query("SELECT * FROM logs ORDER BY time DESC")
protected abstract suspend fun fetch(): MutableList<SuLog>
@Query("DELETE FROM logs WHERE time < :timeout")
protected abstract suspend fun deleteOutdated(timeout: Long = twoWeeksAgo)
@Insert
abstract suspend fun insert(log: SuLog)
}

View file

@ -0,0 +1,54 @@
package com.topjohnwu.magisk.core.data.magiskdb
import com.topjohnwu.magisk.core.ktx.await
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
open class MagiskDB {
class Literal(
val str: String
)
suspend inline fun <R> exec(
query: String,
crossinline mapper: (Map<String, String>) -> R
): List<R> {
return withContext(Dispatchers.IO) {
val out = Shell.cmd("magisk --sqlite '$query'").await().out
out.map { line ->
line.split("\\|".toRegex())
.map { it.split("=", limit = 2) }
.filter { it.size == 2 }
.associate { it[0] to it[1] }
.let(mapper)
}
}
}
suspend fun exec(query: String) {
withContext(Dispatchers.IO) {
Shell.cmd("magisk --sqlite '$query'").await()
}
}
fun Map<String, Any>.toQuery(): String {
val keys = this.keys.joinToString(",")
val values = this.values.joinToString(",") {
when (it) {
is Boolean -> if (it) "1" else "0"
is Number -> it.toString()
is Literal -> it.str
else -> "\"$it\""
}
}
return "($keys) VALUES($values)"
}
object Table {
const val POLICY = "policies"
const val SETTINGS = "settings"
const val STRINGS = "strings"
}
}

View file

@ -0,0 +1,60 @@
package com.topjohnwu.magisk.core.data.magiskdb
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.model.su.SuPolicy
private const val SELECT_QUERY = "SELECT (until - strftime(\"%s\", \"now\")) AS remain, *"
class PolicyDao : MagiskDB() {
suspend fun deleteOutdated() {
val query = "DELETE FROM ${Table.POLICY} WHERE " +
"(until > 0 AND until < strftime(\"%s\", \"now\")) OR until < 0"
exec(query)
}
suspend fun delete(uid: Int) {
val query = "DELETE FROM ${Table.POLICY} WHERE uid=$uid"
exec(query)
}
suspend fun fetch(uid: Int): SuPolicy? {
val query = "$SELECT_QUERY FROM ${Table.POLICY} WHERE uid=$uid LIMIT 1"
return exec(query, ::toPolicy).firstOrNull()
}
suspend fun update(policy: SuPolicy) {
val map = policy.toMap()
if (!Const.Version.atLeast_25_0()) {
// Put in package_name for old database
map["package_name"] = AppContext.packageManager.getNameForUid(policy.uid)!!
}
val query = "REPLACE INTO ${Table.POLICY} ${map.toQuery()}"
exec(query)
}
suspend fun fetchAll(): List<SuPolicy> {
val query = "$SELECT_QUERY FROM ${Table.POLICY} WHERE uid/100000=${Const.USER_ID}"
return exec(query, ::toPolicy).filterNotNull()
}
private fun toPolicy(map: Map<String, String>): SuPolicy? {
val uid = map["uid"]?.toInt() ?: return null
val policy = SuPolicy(uid)
map["until"]?.toLong()?.let { until ->
if (until <= 0) {
policy.remain = until
} else {
map["remain"]?.toLong()?.let { policy.remain = it }
}
}
map["policy"]?.toInt()?.let { policy.policy = it }
map["logging"]?.toInt()?.let { policy.logging = it != 0 }
map["notification"]?.toInt()?.let { policy.notification = it != 0 }
return policy
}
}

View file

@ -0,0 +1,20 @@
package com.topjohnwu.magisk.core.data.magiskdb
class SettingsDao : MagiskDB() {
suspend fun delete(key: String) {
val query = "DELETE FROM ${Table.SETTINGS} WHERE key=\"$key\""
exec(query)
}
suspend fun put(key: String, value: Int) {
val kv = mapOf("key" to key, "value" to value)
val query = "REPLACE INTO ${Table.SETTINGS} ${kv.toQuery()}"
exec(query)
}
suspend fun fetch(key: String, default: Int = -1): Int {
val query = "SELECT value FROM ${Table.SETTINGS} WHERE key=\"$key\" LIMIT 1"
return exec(query) { it["value"]?.toInt() }.firstOrNull() ?: default
}
}

View file

@ -0,0 +1,20 @@
package com.topjohnwu.magisk.core.data.magiskdb
class StringDao : MagiskDB() {
suspend fun delete(key: String) {
val query = "DELETE FROM ${Table.STRINGS} WHERE key=\"$key\""
exec(query)
}
suspend fun put(key: String, value: String) {
val kv = mapOf("key" to key, "value" to value)
val query = "REPLACE INTO ${Table.STRINGS} ${kv.toQuery()}"
exec(query)
}
suspend fun fetch(key: String, default: String = ""): String {
val query = "SELECT value FROM ${Table.STRINGS} WHERE key=\"$key\" LIMIT 1"
return exec(query) { it["value"] }.firstOrNull() ?: default
}
}

View file

@ -0,0 +1,97 @@
package com.topjohnwu.magisk.core.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.topjohnwu.magisk.ProviderInstaller
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.DateTimeAdapter
import com.topjohnwu.magisk.core.utils.LocaleSetting
import okhttp3.Cache
import okhttp3.ConnectionSpec
import okhttp3.Dns
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import java.io.File
import java.net.InetAddress
import java.net.UnknownHostException
private class DnsResolver(client: OkHttpClient) : Dns {
private val doh by lazy {
DnsOverHttps.Builder().client(client)
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(listOf(
InetAddress.getByName("162.159.36.1"),
InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"),
InetAddress.getByName("2606:4700:4700::6400")
))
.resolvePrivateAddresses(true) /* To make PublicSuffixDatabase never used */
.build()
}
override fun lookup(hostname: String): List<InetAddress> {
if (Config.doh) {
try {
return doh.lookup(hostname)
} catch (e: UnknownHostException) {}
}
return Dns.SYSTEM.lookup(hostname)
}
}
fun createOkHttpClient(context: Context): OkHttpClient {
val appCache = Cache(File(context.cacheDir, "okhttp"), 10 * 1024 * 1024)
val builder = OkHttpClient.Builder().cache(appCache)
if (BuildConfig.DEBUG) {
builder.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
})
} else {
builder.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS))
}
builder.dns(DnsResolver(builder.build()))
builder.addInterceptor { chain ->
val request = chain.request().newBuilder()
request.header("User-Agent", "Magisk/${BuildConfig.APP_VERSION_CODE}")
request.header("Accept-Language", LocaleSetting.instance.currentLocale.toLanguageTag())
chain.proceed(request.build())
}
ProviderInstaller.install(context)
return builder.build()
}
fun createMoshiConverterFactory(): MoshiConverterFactory {
val moshi = Moshi.Builder().add(DateTimeAdapter()).build()
return MoshiConverterFactory.create(moshi)
}
fun createRetrofit(okHttpClient: OkHttpClient): Retrofit.Builder {
return Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(createMoshiConverterFactory())
.client(okHttpClient)
}
inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseUrl: String): T {
return retrofitBuilder
.baseUrl(baseUrl)
.build()
.create(T::class.java)
}

View file

@ -0,0 +1,58 @@
package com.topjohnwu.magisk.core.di
import android.annotation.SuppressLint
import android.content.Context
import android.text.method.LinkMovementMethod
import androidx.room.Room
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.data.SuLogDatabase
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
import com.topjohnwu.magisk.core.repository.LogRepository
import com.topjohnwu.magisk.core.repository.NetworkService
import io.noties.markwon.Markwon
import io.noties.markwon.utils.NoCopySpannableFactory
@SuppressLint("StaticFieldLeak")
object ServiceLocator {
val deContext by lazy { AppContext.deviceProtectedContext }
val timeoutPrefs by lazy { deContext.getSharedPreferences("su_timeout", 0) }
// Database
val policyDB = PolicyDao()
val settingsDB = SettingsDao()
val stringDB = StringDao()
val sulogDB by lazy { createSuLogDatabase(deContext).suLogDao() }
val logRepo by lazy { LogRepository(sulogDB) }
// Networking
val okhttp by lazy { createOkHttpClient(AppContext) }
val retrofit by lazy { createRetrofit(okhttp) }
val markwon by lazy { createMarkwon(AppContext) }
val networkService by lazy {
NetworkService(
createApiService(retrofit, Const.Url.INVALID_URL),
createApiService(retrofit, Const.Url.GITHUB_API_URL),
)
}
}
private fun createSuLogDatabase(context: Context) =
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
.addMigrations(SuLogDatabase.MIGRATION_1_2)
.fallbackToDestructiveMigration(true)
.build()
private fun createMarkwon(context: Context) =
Markwon.builder(context).textSetter { textView, spanned, bufferType, onComplete ->
textView.apply {
movementMethod = LinkMovementMethod.getInstance()
setSpannableFactory(NoCopySpannableFactory.getInstance())
setText(spanned, bufferType)
onComplete.run()
}
}.build()

View file

@ -0,0 +1,266 @@
package com.topjohnwu.magisk.core.download
import android.Manifest
import android.annotation.SuppressLint
import android.app.Notification
import android.app.PendingIntent
import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.content.Context
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.collection.SparseArrayCompat
import androidx.collection.isNotEmpty
import androidx.core.content.getSystemService
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.JobService
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.base.IActivityExtension
import com.topjohnwu.magisk.core.cmp
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.ktx.set
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.view.Notifications
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.ResponseBody
import timber.log.Timber
import java.io.InputStream
/**
* This class drives the execution of file downloads and notification management.
*
* Each download engine instance has to be paired with a "session" that is managed by the operating
* system. A session is an Android component that allows executing long lasting operations and
* have its state tied to a notification to show progress.
*
* A session can only have one single notification representing its state, and the operating system
* also uses the notification to manage the lifecycle of a session. One goal of this class is
* to support concurrent download tasks using only one single session, so internally it manages
* all active tasks and notifications and properly re-assign notifications to be attached to
* the session to make sure all download operations can be completed without the operating system
* killing the session.
*
* For API 23 - 33, we use a foreground service as a session.
* For API 34 and higher, we use user-initiated job services as a session.
*/
class DownloadEngine(session: DownloadSession) : DownloadSession by session, DownloadNotifier {
companion object {
const val ACTION = "com.topjohnwu.magisk.DOWNLOAD"
const val SUBJECT_KEY = "subject"
private const val REQUEST_CODE = 1
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
private fun broadcast(progress: Float, subject: Subject) {
progressBroadcast.postValue(progress to subject)
}
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
progressBroadcast.value = null
progressBroadcast.observe(owner) {
val (progress, subject) = it ?: return@observe
callback(progress, subject)
}
}
private fun createBroadcastIntent(context: Context, subject: Subject) =
context.intent<com.topjohnwu.magisk.core.Receiver>()
.setAction(ACTION)
.putExtra(SUBJECT_KEY, subject)
private fun createServiceIntent(context: Context, subject: Subject) =
context.intent<com.topjohnwu.magisk.core.Service>()
.setAction(ACTION)
.putExtra(SUBJECT_KEY, subject)
@SuppressLint("InlinedApi")
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
val flag = PendingIntent.FLAG_IMMUTABLE or
PendingIntent.FLAG_UPDATE_CURRENT or
PendingIntent.FLAG_ONE_SHOT
return if (Build.VERSION.SDK_INT >= 34) {
// On API 34+, download tasks are handled with a user-initiated job.
// However, there is no way to schedule a new job directly with a pending intent.
// As a workaround, we send the subject to a broadcast receiver and have it
// schedule the job for us.
val intent = createBroadcastIntent(context, subject)
PendingIntent.getBroadcast(context, REQUEST_CODE, intent, flag)
} else {
val intent = createServiceIntent(context, subject)
if (Build.VERSION.SDK_INT >= 26) {
PendingIntent.getForegroundService(context, REQUEST_CODE, intent, flag)
} else {
PendingIntent.getService(context, REQUEST_CODE, intent, flag)
}
}
}
@SuppressLint("InlinedApi")
fun <T> startWithActivity(
activity: T,
subject: Subject
) where T : ComponentActivity, T : IActivityExtension {
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
// Always download regardless of notification permission status
start(activity.applicationContext, subject)
}
}
@SuppressLint("MissingPermission")
fun start(context: Context, subject: Subject) {
if (Build.VERSION.SDK_INT >= 34) {
val scheduler = context.getSystemService<JobScheduler>()!!
val cmp = JobService::class.java.cmp(context.packageName)
val extras = Bundle()
extras.putParcelable(SUBJECT_KEY, subject)
val info = JobInfo.Builder(Const.ID.DOWNLOAD_JOB_ID, cmp)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setUserInitiated(true)
.setTransientExtras(extras)
.build()
scheduler.schedule(info)
} else {
val intent = createServiceIntent(context, subject)
if (Build.VERSION.SDK_INT >= 26) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
}
}
private val notifications = SparseArrayCompat<Notification.Builder>()
private var attachedId = -1
private val job = Job()
private val processor = DownloadProcessor(this)
private val network get() = ServiceLocator.networkService
fun download(subject: Subject) {
notifyUpdate(subject.notifyId)
CoroutineScope(job + Dispatchers.IO).launch {
try {
val stream = network.fetchFile(subject.url).toProgressStream(subject)
processor.handle(stream, subject)
val activity = AppContext.foregroundActivity
if (activity != null && subject.autoLaunch) {
notifyRemove(subject.notifyId)
subject.pendingIntent(activity)?.send()
} else {
notifyFinish(subject)
}
} catch (e: Exception) {
Timber.e(e)
notifyFail(subject)
}
}
}
@Synchronized
fun reattach() {
val builder = notifications[attachedId] ?: return
attachNotification(attachedId, builder)
}
private fun attach(id: Int, notification: Notification.Builder) {
attachedId = id
attachNotification(id, notification)
}
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
val notification = notifyRemove(id)?.also(editor) ?: return -1
val newId = Notifications.nextId()
Notifications.mgr.notify(newId, notification.build())
return newId
}
private fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
broadcast(-2f, subject)
it.setContentText(context.getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
}
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
broadcast(1f, subject)
it.setContentTitle(subject.title)
.setContentText(context.getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setProgress(0, 0, false)
.setOngoing(false)
.setAutoCancel(true)
subject.pendingIntent(context)?.let { intent -> it.setContentIntent(intent) }
}
@Synchronized
override fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit) {
val notification = (notifications[id] ?: Notifications.startProgress("").also {
notifications[id] = it
}).apply(editor)
if (attachedId < 0)
attach(id, notification)
else
Notifications.mgr.notify(id, notification.build())
}
@Synchronized
private fun notifyRemove(id: Int): Notification.Builder? {
val idx = notifications.indexOfKey(id)
var n: Notification.Builder? = null
if (idx >= 0) {
n = notifications.valueAt(idx)
notifications.removeAt(idx)
// The cancelled notification is the one attached to the session, need special handling
if (attachedId == id) {
if (notifications.isNotEmpty()) {
// There are still remaining notifications, pick one and attach to the session
val anotherId = notifications.keyAt(0)
val notification = notifications.valueAt(0)
attach(anotherId, notification)
} else {
// No more notifications left, terminate the session
attachedId = -1
onDownloadComplete()
}
}
}
Notifications.mgr.cancel(id)
return n
}
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
val max = contentLength()
val total = max.toFloat() / 1048576
val id = subject.notifyId
notifyUpdate(id) { it.setContentTitle(subject.title) }
return ProgressInputStream(byteStream()) {
val progress = it.toFloat() / 1048576
notifyUpdate(id) { notification ->
if (max > 0) {
broadcast(progress / total, subject)
notification
.setProgress(max.toInt(), it.toInt(), false)
.setContentText("%.2f / %.2f MB".format(progress, total))
} else {
broadcast(-1f, subject)
notification.setContentText("%.2f MB / ??".format(progress))
}
}
}
}
}

View file

@ -0,0 +1,122 @@
package com.topjohnwu.magisk.core.download
import android.net.Uri
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.cachedFile
import com.topjohnwu.magisk.core.ktx.copyAll
import com.topjohnwu.magisk.core.ktx.copyAndClose
import com.topjohnwu.magisk.core.ktx.withInOut
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.utils.APKInstall
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
import org.apache.commons.compress.archivers.zip.ZipFile
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
class DownloadProcessor(notifier: DownloadNotifier) : DownloadNotifier by notifier {
suspend fun handle(stream: InputStream, subject: Subject) {
when (subject) {
is Subject.App -> handleApp(stream, subject)
is Subject.Module -> handleModule(stream, subject.file)
else -> stream.copyAndClose(subject.file.outputStream())
}
}
suspend fun handleApp(stream: InputStream, subject: Subject.App) {
val external = subject.file.outputStream()
if (isRunningAsStub) {
val updateApk = StubApk.update(context)
try {
// Download full APK to stub update path
stream.copyAndClose(TeeOutputStream(external, updateApk.outputStream()))
// Also upgrade stub
notifyUpdate(subject.notifyId) {
it.setProgress(0, 0, true)
.setContentTitle(context.getString(R.string.hide_app_title))
.setContentText("")
}
// Extract stub
val apk = context.cachedFile("stub.apk")
ZipFile.Builder().setFile(updateApk).get().use { zf ->
apk.delete()
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
}
// Patch and install
subject.intent = AppMigration.upgradeStub(context, apk)
?: throw IOException("HideAPK patch error")
apk.delete()
} catch (e: Exception) {
// If any error occurred, do not let stub load the new APK
updateApk.delete()
throw e
}
} else {
val session = APKInstall.startSession(context)
stream.copyAndClose(TeeOutputStream(external, session.openStream(context)))
subject.intent = session.waitIntent()
}
}
suspend fun handleModule(src: InputStream, file: Uri) {
val tmp = context.cachedFile("module.zip")
try {
// First download the entire zip into cache so we can process it
src.writeTo(tmp)
val input = ZipFile.Builder().setFile(tmp).get()
val output = ZipArchiveOutputStream(file.outputStream())
withInOut(input, output) { zin, zout ->
zout.putArchiveEntry(ZipArchiveEntry("META-INF/"))
zout.closeArchiveEntry()
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/"))
zout.closeArchiveEntry()
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/"))
zout.closeArchiveEntry()
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/"))
zout.closeArchiveEntry()
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/update-binary"))
context.assets.open("module_installer.sh").use { it.copyAll(zout) }
zout.closeArchiveEntry()
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/updater-script"))
zout.write("#MAGISK\n".toByteArray())
zout.closeArchiveEntry()
// Then simply copy all entries to output
zin.copyRawEntries(zout) { entry -> !entry.name.startsWith("META-INF") }
}
} finally {
tmp.delete()
}
}
private class TeeOutputStream(
private val o1: OutputStream,
private val o2: OutputStream
) : OutputStream() {
override fun write(b: Int) {
o1.write(b)
o2.write(b)
}
override fun write(b: ByteArray?, off: Int, len: Int) {
o1.write(b, off, len)
o2.write(b, off, len)
}
override fun close() {
o1.close()
o2.close()
}
}
}

View file

@ -0,0 +1,15 @@
package com.topjohnwu.magisk.core.download
import android.app.Notification
import android.content.Context
interface DownloadSession {
val context: Context
fun attachNotification(id: Int, builder: Notification.Builder)
fun onDownloadComplete()
}
interface DownloadNotifier {
val context: Context
fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {})
}

View file

@ -0,0 +1,72 @@
package com.topjohnwu.magisk.core.download
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import androidx.core.net.toUri
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.UpdateInfo
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.view.Notifications
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.io.File
import java.util.UUID
abstract class Subject : Parcelable {
abstract val url: String
abstract val file: Uri
abstract val title: String
abstract val notifyId: Int
open val autoLaunch: Boolean get() = true
open fun pendingIntent(context: Context): PendingIntent? = null
abstract class Module : Subject() {
abstract val module: OnlineModule
final override val url: String get() = module.zipUrl
final override val title: String get() = module.downloadFilename
final override val file by lazy {
MediaStoreUtils.getFile(title).uri
}
}
@Parcelize
class App(
private val json: UpdateInfo = Info.update,
override val notifyId: Int = Notifications.nextId()
) : Subject() {
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
override val url: String get() = json.link
@IgnoredOnParcel
override val file by lazy {
MediaStoreUtils.getFile("${title}.apk").uri
}
@IgnoredOnParcel
var intent: Intent? = null
override fun pendingIntent(context: Context) = intent?.toPending(context)
}
@Parcelize
class Test(
override val notifyId: Int = Notifications.nextId(),
override val title: String = UUID.randomUUID().toString().substring(0, 6)
) : Subject() {
override val url get() = "https://link.testfile.org/250MB"
override val file get() = File("/dev/null").toUri()
override val autoLaunch get() = false
}
@SuppressLint("InlinedApi")
protected fun Intent.toPending(context: Context): PendingIntent {
return PendingIntent.getActivity(context, notifyId, this,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT)
}
}

View file

@ -0,0 +1,149 @@
package com.topjohnwu.magisk.core.ktx
import android.annotation.SuppressLint
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.LayerDrawable
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Process
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.core.content.getSystemService
import com.topjohnwu.magisk.core.utils.LocaleSetting
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.superuser.internal.UiThreadHandler
import java.io.File
fun Context.getBitmap(id: Int): Bitmap {
var drawable = getDrawable(id)!!
if (drawable is BitmapDrawable)
return drawable.bitmap
if (SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable) {
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
}
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth, drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
val Context.deviceProtectedContext: Context get() =
if (SDK_INT >= Build.VERSION_CODES.N) {
createDeviceProtectedStorageContext()
} else { this }
fun Context.cachedFile(name: String) = File(cacheDir, name)
fun ApplicationInfo.getLabel(pm: PackageManager): String {
runCatching {
if (labelRes > 0) {
val res = pm.getResourcesForApplication(this)
LocaleSetting.instance.updateResource(res)
return res.getString(labelRes)
}
}
return loadLabel(pm).toString()
}
fun Context.unwrap(): Context {
var context = this
while (context is ContextWrapper)
context = context.baseContext
return context
}
fun Activity.hideKeyboard() {
val view = currentFocus ?: return
getSystemService<InputMethodManager>()
?.hideSoftInputFromWindow(view.windowToken, 0)
view.clearFocus()
}
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
}
}
@SuppressLint("PrivateApi")
fun getProperty(key: String, def: String): String {
runCatching {
val clazz = Class.forName("android.os.SystemProperties")
val get = clazz.getMethod("get", String::class.java, String::class.java)
return get.invoke(clazz, key, def) as String
}
return def
}
@SuppressLint("InlinedApi")
@Throws(PackageManager.NameNotFoundException::class)
fun PackageManager.getPackageInfo(uid: Int, pid: Int): PackageInfo? {
val flag = PackageManager.MATCH_UNINSTALLED_PACKAGES
val pkgs = getPackagesForUid(uid) ?: throw PackageManager.NameNotFoundException()
if (pkgs.size > 1) {
if (pid <= 0) {
return null
}
// Try to find package name from PID
val proc = RootUtils.getAppProcess(pid)
if (proc == null) {
if (uid == Process.SHELL_UID) {
// It is possible that some apps installed are sharing UID with shell.
// We will not be able to find a package from the active process list,
// because the client is forked from ADB shell, not any app process.
return getPackageInfo("com.android.shell", flag)
}
} else if (uid == proc.uid) {
return getPackageInfo(proc.pkgList[0], flag)
}
return null
}
if (pkgs.size == 1) {
return getPackageInfo(pkgs[0], flag)
}
throw PackageManager.NameNotFoundException()
}
fun Context.registerRuntimeReceiver(receiver: BroadcastReceiver, filter: IntentFilter) {
APKInstall.registerReceiver(this, receiver, filter)
}
fun Context.selfLaunchIntent(): Intent {
val pm = packageManager
val intent = pm.getLaunchIntentForPackage(packageName)!!
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
return intent
}
fun Context.toast(msg: CharSequence, duration: Int) {
UiThreadHandler.run { Toast.makeText(this, msg, duration).show() }
}
fun Context.toast(resId: Int, duration: Int) {
UiThreadHandler.run { Toast.makeText(this, resId, duration).show() }
}

View file

@ -0,0 +1,98 @@
package com.topjohnwu.magisk.core.ktx
import androidx.collection.SparseArrayCompat
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import java.io.Closeable
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.lang.reflect.Field
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Collections
inline fun <In : Closeable, Out : Closeable> withInOut(
input: In,
output: Out,
withBoth: (In, Out) -> Unit
) {
input.use { reader ->
output.use { writer ->
withBoth(reader, writer)
}
}
}
@Throws(IOException::class)
suspend fun InputStream.copyAll(
out: OutputStream,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
dispatcher: CoroutineDispatcher = Dispatchers.IO
): Long {
return withContext(dispatcher) {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = read(buffer)
while (isActive && bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = read(buffer)
}
bytesCopied
}
}
@Throws(IOException::class)
suspend inline fun InputStream.copyAndClose(
out: OutputStream,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) = withInOut(this, out) { i, o -> i.copyAll(o, bufferSize, dispatcher) }
@Throws(IOException::class)
suspend inline fun InputStream.writeTo(
file: File,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) = copyAndClose(file.outputStream(), bufferSize, dispatcher)
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
put(key, value)
}
fun <T> MutableList<T>.synchronized(): MutableList<T> = Collections.synchronizedList(this)
fun <T> MutableSet<T>.synchronized(): MutableSet<T> = Collections.synchronizedSet(this)
fun <K, V> MutableMap<K, V>.synchronized(): MutableMap<K, V> = Collections.synchronizedMap(this)
fun Class<*>.reflectField(name: String): Field =
getDeclaredField(name).apply { isAccessible = true }
inline fun <T, R> Flow<T>.concurrentMap(crossinline transform: suspend (T) -> R): Flow<R> {
return flatMapMerge { value ->
flow { emit(transform(value)) }
}
}
fun Long.toTime(format: DateTimeFormatter): String = format.format(Instant.ofEpochMilli(this))
// Some devices don't allow filenames containing ":"
val timeFormatStandard: DateTimeFormatter by lazy {
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH.mm.ss").withZone(ZoneId.systemDefault())
}
val timeDateFormat: DateTimeFormatter by lazy {
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withZone(ZoneId.systemDefault())
}
val dateFormat: DateTimeFormatter by lazy {
DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withZone(ZoneId.systemDefault())
}

View file

@ -0,0 +1,16 @@
package com.topjohnwu.magisk.core.ktx
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
fun reboot(reason: String = if (Config.recovery) "recovery" else "") {
if (reason == "recovery") {
// KEYCODE_POWER = 26, hide incorrect "Factory data reset" message
Shell.cmd("/system/bin/input keyevent 26").submit()
}
Shell.cmd("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
}
suspend fun Shell.Job.await() = withContext(Dispatchers.IO) { exec() }

View file

@ -0,0 +1,68 @@
package com.topjohnwu.magisk.core.model
import android.os.Parcelable
import com.squareup.moshi.FromJson
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.ToJson
import kotlinx.parcelize.Parcelize
import java.time.Instant
@JsonClass(generateAdapter = true)
class UpdateJson(
val magisk: UpdateInfo = UpdateInfo(),
)
@Parcelize
@JsonClass(generateAdapter = true)
data class UpdateInfo(
val version: String = "",
val versionCode: Int = -1,
val link: String = "",
val note: String = ""
) : Parcelable
@JsonClass(generateAdapter = true)
data class ModuleJson(
val version: String,
val versionCode: Int,
val zipUrl: String,
val changelog: String,
)
@JsonClass(generateAdapter = true)
data class ReleaseAssets(
val name: String,
@param:Json(name = "browser_download_url") val url: String,
)
class DateTimeAdapter {
@ToJson
fun toJson(date: Instant): String {
return date.toString()
}
@FromJson
fun fromJson(date: String): Instant {
return Instant.parse(date)
}
}
@JsonClass(generateAdapter = true)
data class Release(
@param:Json(name = "tag_name") val tag: String,
val name: String,
val prerelease: Boolean,
val assets: List<ReleaseAssets>,
val body: String,
@param:Json(name = "created_at") val createdTime: Instant,
) {
val versionCode: Int get() {
return if (tag[0] == 'v') {
(tag.drop(1).toFloat() * 1000).toInt()
} else {
tag.drop(7).toInt()
}
}
}

View file

@ -0,0 +1,135 @@
package com.topjohnwu.magisk.core.model.module
import com.squareup.moshi.JsonDataException
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.nio.ExtendedFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
import java.util.Locale
data class LocalModule(
val base: ExtendedFile,
) : Module() {
private val svc get() = ServiceLocator.networkService
override var id: String = ""
override var name: String = ""
override var version: String = ""
override var versionCode: Int = -1
var author: String = ""
var description: String = ""
var updateInfo: OnlineModule? = null
var outdated = false
private var updateUrl: String = ""
private val removeFile = base.getChildFile("remove")
private val disableFile = base.getChildFile("disable")
private val updateFile = base.getChildFile("update")
val zygiskFolder = base.getChildFile("zygisk")
val updated get() = updateFile.exists()
val isRiru = (id == "riru-core") || base.getChildFile("riru").exists()
val isZygisk = zygiskFolder.exists()
val zygiskUnloaded = zygiskFolder.getChildFile("unloaded").exists()
val hasAction = base.getChildFile("action.sh").exists()
var enable: Boolean
get() = !disableFile.exists()
set(enable) {
if (enable) {
disableFile.delete()
Shell.cmd("copy_preinit_files").submit()
} else {
!disableFile.createNewFile()
Shell.cmd("copy_preinit_files").submit()
}
}
var remove: Boolean
get() = removeFile.exists()
set(remove) {
if (remove) {
if (updateFile.exists()) return
removeFile.createNewFile()
Shell.cmd("copy_preinit_files").submit()
} else {
removeFile.delete()
Shell.cmd("copy_preinit_files").submit()
}
}
@Throws(NumberFormatException::class)
private fun parseProps(props: List<String>) {
for (line in props) {
val prop = line.split("=".toRegex(), 2).map { it.trim() }
if (prop.size != 2)
continue
val key = prop[0]
val value = prop[1]
if (key.isEmpty() || key[0] == '#')
continue
when (key) {
"id" -> id = value
"name" -> name = value
"version" -> version = value
"versionCode" -> versionCode = value.toInt()
"author" -> author = value
"description" -> description = value
"updateJson" -> updateUrl = value
}
}
}
init {
runCatching {
parseProps(Shell.cmd("dos2unix < $base/module.prop").exec().out)
}
if (id.isEmpty()) {
id = base.name
}
if (name.isEmpty()) {
name = id
}
}
suspend fun fetch(): Boolean {
if (updateUrl.isEmpty())
return false
try {
val json = svc.fetchModuleJson(updateUrl)
updateInfo = OnlineModule(this, json)
outdated = json.versionCode > versionCode
return true
} catch (e: IOException) {
Timber.w(e)
} catch (e: JsonDataException) {
Timber.w(e)
}
return false
}
companion object {
fun loaded() = RootUtils.fs.getFile(Const.MODULE_PATH).exists()
suspend fun installed() = withContext(Dispatchers.IO) {
RootUtils.fs.getFile(Const.MODULE_PATH)
.listFiles()
.orEmpty()
.filter { !it.isFile && !it.isHidden }
.map { LocalModule(it) }
.sortedBy { it.name.lowercase(Locale.ROOT) }
}
}
}

View file

@ -0,0 +1,14 @@
package com.topjohnwu.magisk.core.model.module
abstract class Module : Comparable<Module> {
abstract var id: String
protected set
abstract var name: String
protected set
abstract var version: String
protected set
abstract var versionCode: Int
protected set
override operator fun compareTo(other: Module) = id.compareTo(other.id)
}

View file

@ -0,0 +1,27 @@
package com.topjohnwu.magisk.core.model.module
import android.os.Parcelable
import com.topjohnwu.magisk.core.model.ModuleJson
import kotlinx.parcelize.Parcelize
@Parcelize
data class OnlineModule(
override var id: String,
override var name: String,
override var version: String,
override var versionCode: Int,
val zipUrl: String,
val changelog: String,
) : Module(), Parcelable {
constructor(local: LocalModule, json: ModuleJson) :
this(local.id, local.name, json.version, json.versionCode, json.zipUrl, json.changelog)
val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename()
private fun String.legalFilename() = replace(" ", "_")
.replace("'", "").replace("\"", "")
.replace("$", "").replace("`", "")
.replace("*", "").replace("/", "_")
.replace("#", "").replace("@", "")
.replace("\\", "_")
}

View file

@ -0,0 +1,72 @@
package com.topjohnwu.magisk.core.model.su
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.core.ktx.getLabel
@Entity(tableName = "logs")
class SuLog(
val fromUid: Int,
val toUid: Int,
val fromPid: Int,
val packageName: String,
val appName: String,
val command: String,
val action: Int,
val target: Int,
val context: String,
val gids: String,
val time: Long = System.currentTimeMillis()
) {
@PrimaryKey(autoGenerate = true) var id: Int = 0
}
fun PackageManager.createSuLog(
info: ApplicationInfo,
toUid: Int,
fromPid: Int,
command: String,
policy: Int,
target: Int,
context: String,
gids: String,
): SuLog {
return SuLog(
fromUid = info.uid,
toUid = toUid,
fromPid = fromPid,
packageName = getNameForUid(info.uid)!!,
appName = info.getLabel(this),
command = command,
action = policy,
target = target,
context = context,
gids = gids,
)
}
fun createSuLog(
fromUid: Int,
toUid: Int,
fromPid: Int,
command: String,
policy: Int,
target: Int,
context: String,
gids: String,
): SuLog {
return SuLog(
fromUid = fromUid,
toUid = toUid,
fromPid = fromPid,
packageName = "[UID] $fromUid",
appName = "[UID] $fromUid",
command = command,
action = policy,
target = target,
context = context,
gids = gids,
)
}

View file

@ -0,0 +1,33 @@
package com.topjohnwu.magisk.core.model.su
import com.topjohnwu.magisk.core.data.magiskdb.MagiskDB
class SuPolicy(
val uid: Int,
var policy: Int = QUERY,
var remain: Long = -1L,
var logging: Boolean = true,
var notification: Boolean = true,
) {
companion object {
const val QUERY = 0
const val DENY = 1
const val ALLOW = 2
const val RESTRICT = 3
}
fun toMap(): MutableMap<String, Any> {
val until = if (remain <= 0) {
remain
} else {
MagiskDB.Literal("(strftime(\"%s\", \"now\") + $remain)")
}
return mutableMapOf(
"uid" to uid,
"policy" to policy,
"until" to until,
"logging" to logging,
"notification" to notification
)
}
}

View file

@ -0,0 +1,115 @@
package com.topjohnwu.magisk.core.repository
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
interface DBConfig {
val settingsDB: SettingsDao
val stringDB: StringDao
val coroutineScope: CoroutineScope
fun dbSettings(
name: String,
default: Int
) = IntDBProperty(name, default)
fun dbSettings(
name: String,
default: Boolean
) = BoolDBProperty(name, default)
fun dbStrings(
name: String,
default: String,
sync: Boolean = false
) = StringDBProperty(name, default, sync)
}
class IntDBProperty(
private val name: String,
private val default: Int
) : ReadWriteProperty<DBConfig, Int> {
var value: Int? = null
@Synchronized
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
if (value == null)
value = runBlocking { thisRef.settingsDB.fetch(name, default) }
return value as Int
}
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Int) {
synchronized(this) {
this.value = value
}
thisRef.coroutineScope.launch {
thisRef.settingsDB.put(name, value)
}
}
}
open class BoolDBProperty(
name: String,
default: Boolean
) : ReadWriteProperty<DBConfig, Boolean> {
val base = IntDBProperty(name, if (default) 1 else 0)
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
base.getValue(thisRef, property) != 0
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) =
base.setValue(thisRef, property, if (value) 1 else 0)
}
class StringDBProperty(
private val name: String,
private val default: String,
private val sync: Boolean
) : ReadWriteProperty<DBConfig, String> {
private var value: String? = null
@Synchronized
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
if (value == null)
value = runBlocking {
thisRef.stringDB.fetch(name, default)
}
return value!!
}
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: String) {
synchronized(this) {
this.value = value
}
if (value.isEmpty()) {
if (sync) {
runBlocking {
thisRef.stringDB.delete(name)
}
} else {
thisRef.coroutineScope.launch {
thisRef.stringDB.delete(name)
}
}
} else {
if (sync) {
runBlocking {
thisRef.stringDB.put(name, value)
}
} else {
thisRef.coroutineScope.launch {
thisRef.stringDB.put(name, value)
}
}
}
}
}

View file

@ -0,0 +1,46 @@
package com.topjohnwu.magisk.core.repository
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.data.SuLogDao
import com.topjohnwu.magisk.core.ktx.await
import com.topjohnwu.magisk.core.model.su.SuLog
import com.topjohnwu.superuser.Shell
class LogRepository(
private val logDao: SuLogDao
) {
suspend fun fetchSuLogs() = logDao.fetchAll()
suspend fun fetchMagiskLogs(): String {
val list = object : AbstractMutableList<String>() {
val buf = StringBuilder()
override val size get() = 0
override fun get(index: Int): String = ""
override fun removeAt(index: Int): String = ""
override fun set(index: Int, element: String): String = ""
override fun add(index: Int, element: String) {
if (element.isNotEmpty()) {
buf.append(element)
buf.append('\n')
}
}
}
if (Info.env.isActive) {
Shell.cmd("cat ${Const.MAGISK_LOG} || logcat -d -s Magisk").to(list).await()
} else {
Shell.cmd("logcat -d").to(list).await()
}
return list.buf.toString()
}
suspend fun clearLogs() = logDao.deleteAll()
fun clearMagiskLogs(cb: (Shell.Result) -> Unit) =
Shell.cmd("echo -n > ${Const.MAGISK_LOG}").submit(cb)
suspend fun insert(log: SuLog) = logDao.insert(log)
}

View file

@ -0,0 +1,137 @@
package com.topjohnwu.magisk.core.repository
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.CUSTOM_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.DEBUG_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.data.GithubApiServices
import com.topjohnwu.magisk.core.data.RawUrl
import com.topjohnwu.magisk.core.ktx.dateFormat
import com.topjohnwu.magisk.core.model.Release
import com.topjohnwu.magisk.core.model.ReleaseAssets
import com.topjohnwu.magisk.core.model.UpdateInfo
import retrofit2.HttpException
import timber.log.Timber
import java.io.IOException
class NetworkService(
private val raw: RawUrl,
private val api: GithubApiServices,
) {
suspend fun fetchUpdate() = safe {
var info = when (Config.updateChannel) {
DEFAULT_CHANNEL -> if (BuildConfig.DEBUG) fetchDebugUpdate() else fetchStableUpdate()
STABLE_CHANNEL -> fetchStableUpdate()
BETA_CHANNEL -> fetchBetaUpdate()
DEBUG_CHANNEL -> fetchDebugUpdate()
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
else -> throw IllegalArgumentException()
}
if (info.versionCode < Info.env.versionCode &&
Config.updateChannel == DEFAULT_CHANNEL &&
!BuildConfig.DEBUG
) {
Config.updateChannel = BETA_CHANNEL
info = fetchBetaUpdate()
}
info
}
suspend fun fetchUpdate(version: Int) = safe {
findRelease { it.versionCode == version }.asInfo()
}
// Keep going through all release pages until we find a match
private suspend inline fun findRelease(predicate: (Release) -> Boolean): Release? {
var page = 1
while (true) {
val response = api.fetchReleases(page = page)
val releases = response.body() ?: throw HttpException(response)
// Remove all non Magisk releases
releases.removeAll { it.tag[0] != 'v' && !it.tag.startsWith("canary") }
// Make sure it's sorted correctly
releases.sortByDescending { it.createdTime }
releases.find(predicate)?.let { return it }
if (response.headers()["link"]?.contains("rel=\"next\"", ignoreCase = true) == true) {
page += 1
} else {
return null
}
}
}
private inline fun Release?.asInfo(
selector: (ReleaseAssets) -> Boolean = {
// Default selector picks the non-debug APK
it.name.run { endsWith(".apk") && !contains("debug") }
}): UpdateInfo {
return if (this == null) UpdateInfo()
else if (tag[0] == 'v') asPublicInfo(selector)
else asCanaryInfo(selector)
}
private inline fun Release.asPublicInfo(selector: (ReleaseAssets) -> Boolean): UpdateInfo {
val version = tag.drop(1)
val date = dateFormat.format(createdTime)
return UpdateInfo(
version = version,
versionCode = versionCode,
link = assets.find(selector)!!.url,
note = "## $date $name\n\n$body"
)
}
private inline fun Release.asCanaryInfo(selector: (ReleaseAssets) -> Boolean): UpdateInfo {
return UpdateInfo(
version = name.substring(8, 16),
versionCode = versionCode,
link = assets.find(selector)!!.url,
note = "## $name\n\n$body"
)
}
// Version number: debug == beta >= stable
// Find the latest non-prerelease
private suspend fun fetchStableUpdate() = api.fetchLatestRelease().asInfo()
// Find the latest release, regardless whether it's prerelease
private suspend fun fetchBetaUpdate() = findRelease { true }.asInfo()
private suspend fun fetchDebugUpdate() =
findRelease { true }.asInfo { it.name == "app-debug.apk" }
private suspend fun fetchCustomUpdate(url: String): UpdateInfo {
val info = raw.fetchUpdateJson(url).magisk
return info.let { it.copy(note = raw.fetchString(it.note)) }
}
private inline fun <T> safe(factory: () -> T): T? {
return try {
if (Info.isConnected.value == true)
factory()
else
null
} catch (e: Exception) {
Timber.e(e)
null
}
}
private inline fun <T> wrap(factory: () -> T): T {
return try {
factory()
} catch (e: HttpException) {
throw IOException(e)
}
}
// Fetch files
suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) }
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
suspend fun fetchModuleJson(url: String) = wrap { raw.fetchModuleJson(url) }
}

View file

@ -0,0 +1,139 @@
package com.topjohnwu.magisk.core.repository
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
interface PreferenceConfig {
val context: Context
val fileName: String
get() = "${context.packageName}_preferences"
val prefs: SharedPreferences
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
fun preferenceStrInt(
name: String,
default: Int,
commit: Boolean = false
) = object: ReadWriteProperty<PreferenceConfig, Int> {
val base = StringProperty(name, default.toString(), commit)
override fun getValue(thisRef: PreferenceConfig, property: KProperty<*>): Int =
base.getValue(thisRef, property).toInt()
override fun setValue(thisRef: PreferenceConfig, property: KProperty<*>, value: Int) =
base.setValue(thisRef, property, value.toString())
}
fun preference(
name: String,
default: Boolean,
commit: Boolean = false
) = BooleanProperty(name, default, commit)
fun preference(
name: String,
default: Int,
commit: Boolean = false
) = IntProperty(name, default, commit)
fun preference(
name: String,
default: String,
commit: Boolean = false
) = StringProperty(name, default, commit)
}
abstract class PreferenceProperty {
fun SharedPreferences.Editor.put(name: String, value: Boolean) = putBoolean(name, value)
fun SharedPreferences.Editor.put(name: String, value: Float) = putFloat(name, value)
fun SharedPreferences.Editor.put(name: String, value: Int) = putInt(name, value)
fun SharedPreferences.Editor.put(name: String, value: Long) = putLong(name, value)
fun SharedPreferences.Editor.put(name: String, value: String) = putString(name, value)
fun SharedPreferences.Editor.put(name: String, value: Set<String>) = putStringSet(name, value)
fun SharedPreferences.get(name: String, value: Boolean) = getBoolean(name, value)
fun SharedPreferences.get(name: String, value: Float) = getFloat(name, value)
fun SharedPreferences.get(name: String, value: Int) = getInt(name, value)
fun SharedPreferences.get(name: String, value: Long) = getLong(name, value)
fun SharedPreferences.get(name: String, value: String) = getString(name, value) ?: value
fun SharedPreferences.get(name: String, value: Set<String>) = getStringSet(name, value) ?: value
}
class BooleanProperty(
private val name: String,
private val default: Boolean,
private val commit: Boolean
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Boolean> {
override operator fun getValue(
thisRef: PreferenceConfig,
property: KProperty<*>
): Boolean {
val prefName = name.ifBlank { property.name }
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceConfig,
property: KProperty<*>,
value: Boolean
) {
val prefName = name.ifBlank { property.name }
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}
class IntProperty(
private val name: String,
private val default: Int,
private val commit: Boolean
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Int> {
override operator fun getValue(
thisRef: PreferenceConfig,
property: KProperty<*>
): Int {
val prefName = name.ifBlank { property.name }
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceConfig,
property: KProperty<*>,
value: Int
) {
val prefName = name.ifBlank { property.name }
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}
class StringProperty(
private val name: String,
private val default: String,
private val commit: Boolean
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, String> {
override operator fun getValue(
thisRef: PreferenceConfig,
property: KProperty<*>
): String {
val prefName = name.ifBlank { property.name }
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceConfig,
property: KProperty<*>,
value: String
) {
val prefName = name.ifBlank { property.name }
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View file

@ -0,0 +1,772 @@
package com.topjohnwu.magisk.core.signing;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.DigestException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* APK Signature Scheme v2 signer.
*
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
* uncompressed contents of ZIP entries.
*/
public abstract class ApkSignerV2 {
/*
* The two main goals of APK Signature Scheme v2 are:
* 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
* cover every byte of the APK being signed.
* 2. Enable much faster signature and integrity verification. This is achieved by requiring
* only a minimal amount of APK parsing before the signature is verified, thus completely
* bypassing ZIP entry decompression and by making integrity verification parallelizable by
* employing a hash tree.
*
* The generated signature block is wrapped into an APK Signing Block and inserted into the
* original APK immediately before the start of ZIP Central Directory. This is to ensure that
* JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
* extensibility. For example, a future signature scheme could insert its signatures there as
* well. The contract of the APK Signing Block is that all contents outside of the block must be
* protected by signatures inside the block.
*/
public static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101;
public static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102;
public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103;
public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104;
public static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201;
public static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202;
public static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301;
public static final int SIGNATURE_DSA_WITH_SHA512 = 0x0302;
/**
* {@code .SF} file header section attribute indicating that the APK is signed not just with
* JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute
* facilitates v2 signature stripping detection.
*
* <p>The attribute contains a comma-separated set of signature scheme IDs.
*/
public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE = "2";
private static final int CONTENT_DIGEST_CHUNKED_SHA256 = 0;
private static final int CONTENT_DIGEST_CHUNKED_SHA512 = 1;
private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
private static final byte[] APK_SIGNING_BLOCK_MAGIC =
new byte[] {
0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
};
private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
private ApkSignerV2() {}
/**
* Signer configuration.
*/
public static final class SignerConfig {
/** Private key. */
public PrivateKey privateKey;
/**
* Certificates, with the first certificate containing the public key corresponding to
* {@link #privateKey}.
*/
public List<X509Certificate> certificates;
/**
* List of signature algorithms with which to sign (see {@code SIGNATURE_...} constants).
*/
public List<Integer> signatureAlgorithms;
}
/**
* Signs the provided APK using APK Signature Scheme v2 and returns the signed APK as a list of
* consecutive chunks.
*
* <p>NOTE: To enable APK signature verifier to detect v2 signature stripping, header sections
* of META-INF/*.SF files of APK being signed must contain the
* {@code X-Android-APK-Signed: true} attribute.
*
* @param inputApk contents of the APK to be signed. The APK starts at the current position
* of the buffer and ends at the limit of the buffer.
* @param signerConfigs signer configurations, one for each signer.
*
* @throws ApkParseException if the APK cannot be parsed.
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
* cannot be used in general.
* @throws SignatureException if an error occurs when computing digests of generating
* signatures.
*/
public static ByteBuffer[] sign(
ByteBuffer inputApk,
List<SignerConfig> signerConfigs)
throws ApkParseException, InvalidKeyException, SignatureException {
// Slice/create a view in the inputApk to make sure that:
// 1. inputApk is what's between position and limit of the original inputApk, and
// 2. changes to position, limit, and byte order are not reflected in the original.
ByteBuffer originalInputApk = inputApk;
inputApk = originalInputApk.slice();
inputApk.order(ByteOrder.LITTLE_ENDIAN);
// Locate ZIP End of Central Directory (EoCD), Central Directory, and check that Central
// Directory is immediately followed by the ZIP End of Central Directory.
int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(inputApk);
if (eocdOffset == -1) {
throw new ApkParseException("Failed to locate ZIP End of Central Directory");
}
if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(inputApk, eocdOffset)) {
throw new ApkParseException("ZIP64 format not supported");
}
inputApk.position(eocdOffset);
long centralDirSizeLong = ZipUtils.getZipEocdCentralDirectorySizeBytes(inputApk);
if (centralDirSizeLong > Integer.MAX_VALUE) {
throw new ApkParseException(
"ZIP Central Directory size out of range: " + centralDirSizeLong);
}
int centralDirSize = (int) centralDirSizeLong;
long centralDirOffsetLong = ZipUtils.getZipEocdCentralDirectoryOffset(inputApk);
if (centralDirOffsetLong > Integer.MAX_VALUE) {
throw new ApkParseException(
"ZIP Central Directory offset in file out of range: " + centralDirOffsetLong);
}
int centralDirOffset = (int) centralDirOffsetLong;
int expectedEocdOffset = centralDirOffset + centralDirSize;
if (expectedEocdOffset < centralDirOffset) {
throw new ApkParseException(
"ZIP Central Directory extent too large. Offset: " + centralDirOffset
+ ", size: " + centralDirSize);
}
if (eocdOffset != expectedEocdOffset) {
throw new ApkParseException(
"ZIP Central Directory not immeiately followed by ZIP End of"
+ " Central Directory. CD end: " + expectedEocdOffset
+ ", EoCD start: " + eocdOffset);
}
// Create ByteBuffers holding the contents of everything before ZIP Central Directory,
// ZIP Central Directory, and ZIP End of Central Directory.
inputApk.clear();
ByteBuffer beforeCentralDir = getByteBuffer(inputApk, centralDirOffset);
ByteBuffer centralDir = getByteBuffer(inputApk, eocdOffset - centralDirOffset);
// Create a copy of End of Central Directory because we'll need modify its contents later.
byte[] eocdBytes = new byte[inputApk.remaining()];
inputApk.get(eocdBytes);
ByteBuffer eocd = ByteBuffer.wrap(eocdBytes);
eocd.order(inputApk.order());
// Figure which which digests to use for APK contents.
Set<Integer> contentDigestAlgorithms = new HashSet<>();
for (SignerConfig signerConfig : signerConfigs) {
for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
contentDigestAlgorithms.add(
getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm));
}
}
// Compute digests of APK contents.
Map<Integer, byte[]> contentDigests; // digest algorithm ID -> digest
try {
contentDigests =
computeContentDigests(
contentDigestAlgorithms,
new ByteBuffer[] {beforeCentralDir, centralDir, eocd});
} catch (DigestException e) {
throw new SignatureException("Failed to compute digests of APK", e);
}
// Sign the digests and wrap the signatures and signer info into an APK Signing Block.
ByteBuffer apkSigningBlock =
ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests));
// Update Central Directory Offset in End of Central Directory Record. Central Directory
// follows the APK Signing Block and thus is shifted by the size of the APK Signing Block.
centralDirOffset += apkSigningBlock.remaining();
eocd.clear();
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset);
// Follow the Java NIO pattern for ByteBuffer whose contents have been consumed.
originalInputApk.position(originalInputApk.limit());
// Reset positions (to 0) and limits (to capacity) in the ByteBuffers below to follow the
// Java NIO pattern for ByteBuffers which are ready for their contents to be read by caller.
// Contrary to the name, this does not clear the contents of these ByteBuffer.
beforeCentralDir.clear();
centralDir.clear();
eocd.clear();
// Insert APK Signing Block immediately before the ZIP Central Directory.
return new ByteBuffer[] {
beforeCentralDir,
apkSigningBlock,
centralDir,
eocd,
};
}
private static Map<Integer, byte[]> computeContentDigests(
Set<Integer> digestAlgorithms,
ByteBuffer[] contents) throws DigestException {
// For each digest algorithm the result is computed as follows:
// 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
// The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
// No chunks are produced for empty (zero length) segments.
// 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
// length in bytes (uint32 little-endian) and the chunk's contents.
// 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
// chunks (uint32 little-endian) and the concatenation of digests of chunks of all
// segments in-order.
int chunkCount = 0;
for (ByteBuffer input : contents) {
chunkCount += getChunkCount(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
}
final Map<Integer, byte[]> digestsOfChunks = new HashMap<>(digestAlgorithms.size());
for (int digestAlgorithm : digestAlgorithms) {
int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
byte[] concatenationOfChunkCountAndChunkDigests =
new byte[5 + chunkCount * digestOutputSizeBytes];
concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
setUnsignedInt32LittleEngian(
chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests);
}
int chunkIndex = 0;
byte[] chunkContentPrefix = new byte[5];
chunkContentPrefix[0] = (byte) 0xa5;
// Optimization opportunity: digests of chunks can be computed in parallel.
for (ByteBuffer input : contents) {
while (input.hasRemaining()) {
int chunkSize =
Math.min(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
final ByteBuffer chunk = getByteBuffer(input, chunkSize);
for (int digestAlgorithm : digestAlgorithms) {
String jcaAlgorithmName =
getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
MessageDigest md;
try {
md = MessageDigest.getInstance(jcaAlgorithmName);
} catch (NoSuchAlgorithmException e) {
throw new DigestException(
jcaAlgorithmName + " MessageDigest not supported", e);
}
// Reset position to 0 and limit to capacity. Position would've been modified
// by the preceding iteration of this loop. NOTE: Contrary to the method name,
// this does not modify the contents of the chunk.
chunk.clear();
setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1);
md.update(chunkContentPrefix);
md.update(chunk);
byte[] concatenationOfChunkCountAndChunkDigests =
digestsOfChunks.get(digestAlgorithm);
int expectedDigestSizeBytes =
getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
int actualDigestSizeBytes =
md.digest(
concatenationOfChunkCountAndChunkDigests,
5 + chunkIndex * expectedDigestSizeBytes,
expectedDigestSizeBytes);
if (actualDigestSizeBytes != expectedDigestSizeBytes) {
throw new DigestException(
"Unexpected output size of " + md.getAlgorithm()
+ " digest: " + actualDigestSizeBytes);
}
}
chunkIndex++;
}
}
Map<Integer, byte[]> result = new HashMap<>(digestAlgorithms.size());
for (Map.Entry<Integer, byte[]> entry : digestsOfChunks.entrySet()) {
int digestAlgorithm = entry.getKey();
byte[] concatenationOfChunkCountAndChunkDigests = entry.getValue();
String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
MessageDigest md;
try {
md = MessageDigest.getInstance(jcaAlgorithmName);
} catch (NoSuchAlgorithmException e) {
throw new DigestException(jcaAlgorithmName + " MessageDigest not supported", e);
}
result.put(digestAlgorithm, md.digest(concatenationOfChunkCountAndChunkDigests));
}
return result;
}
private static int getChunkCount(int inputSize, int chunkSize) {
return (inputSize + chunkSize - 1) / chunkSize;
}
private static void setUnsignedInt32LittleEngian(int value, byte[] result, int offset) {
result[offset] = (byte) (value & 0xff);
result[offset + 1] = (byte) ((value >> 8) & 0xff);
result[offset + 2] = (byte) ((value >> 16) & 0xff);
result[offset + 3] = (byte) ((value >> 24) & 0xff);
}
private static byte[] generateApkSigningBlock(
List<SignerConfig> signerConfigs,
Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
byte[] apkSignatureSchemeV2Block =
generateApkSignatureSchemeV2Block(signerConfigs, contentDigests);
return generateApkSigningBlock(apkSignatureSchemeV2Block);
}
private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
// FORMAT:
// uint64: size (excluding this field)
// repeated ID-value pairs:
// uint64: size (excluding this field)
// uint32: ID
// (size - 4) bytes: value
// uint64: size (same as the one above)
// uint128: magic
int resultSize =
8 // size
+ 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
+ 8 // size
+ 16 // magic
;
ByteBuffer result = ByteBuffer.allocate(resultSize);
result.order(ByteOrder.LITTLE_ENDIAN);
long blockSizeFieldValue = resultSize - 8;
result.putLong(blockSizeFieldValue);
long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
result.putLong(pairSizeFieldValue);
result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
result.put(apkSignatureSchemeV2Block);
result.putLong(blockSizeFieldValue);
result.put(APK_SIGNING_BLOCK_MAGIC);
return result.array();
}
private static byte[] generateApkSignatureSchemeV2Block(
List<SignerConfig> signerConfigs,
Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
// FORMAT:
// * length-prefixed sequence of length-prefixed signer blocks.
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
int signerNumber = 0;
for (SignerConfig signerConfig : signerConfigs) {
signerNumber++;
byte[] signerBlock;
try {
signerBlock = generateSignerBlock(signerConfig, contentDigests);
} catch (InvalidKeyException e) {
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
} catch (SignatureException e) {
throw new SignatureException("Signer #" + signerNumber + " failed", e);
}
signerBlocks.add(signerBlock);
}
return encodeAsSequenceOfLengthPrefixedElements(
new byte[][] {
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
});
}
private static byte[] generateSignerBlock(
SignerConfig signerConfig,
Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
if (signerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
byte[] encodedPublicKey = encodePublicKey(publicKey);
V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
try {
signedData.certificates = encodeCertificates(signerConfig.certificates);
} catch (CertificateEncodingException e) {
throw new SignatureException("Failed to encode certificates", e);
}
List<Pair<Integer, byte[]>> digests =
new ArrayList<>(signerConfig.signatureAlgorithms.size());
for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
int contentDigestAlgorithm =
getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm);
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
if (contentDigest == null) {
throw new RuntimeException(
getContentDigestAlgorithmJcaDigestAlgorithm(contentDigestAlgorithm)
+ " content digest for "
+ getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm)
+ " not computed");
}
digests.add(Pair.create(signatureAlgorithm, contentDigest));
}
signedData.digests = digests;
V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
// FORMAT:
// * length-prefixed sequence of length-prefixed digests:
// * uint32: signature algorithm ID
// * length-prefixed bytes: digest of contents
// * length-prefixed sequence of certificates:
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
// * length-prefixed sequence of length-prefixed additional attributes:
// * uint32: ID
// * (length - 4) bytes: value
signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] {
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests),
encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
// additional attributes
new byte[0],
});
signer.publicKey = encodedPublicKey;
signer.signatures = new ArrayList<>();
for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
Pair<String, ? extends AlgorithmParameterSpec> signatureParams =
getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm);
String jcaSignatureAlgorithm = signatureParams.getFirst();
AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureParams.getSecond();
byte[] signatureBytes;
try {
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
signature.initSign(signerConfig.privateKey);
if (jcaSignatureAlgorithmParams != null) {
signature.setParameter(jcaSignatureAlgorithmParams);
}
signature.update(signer.signedData);
signatureBytes = signature.sign();
} catch (InvalidKeyException e) {
throw new InvalidKeyException("Failed sign using " + jcaSignatureAlgorithm, e);
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
| SignatureException e) {
throw new SignatureException("Failed sign using " + jcaSignatureAlgorithm, e);
}
try {
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
signature.initVerify(publicKey);
if (jcaSignatureAlgorithmParams != null) {
signature.setParameter(jcaSignatureAlgorithmParams);
}
signature.update(signer.signedData);
if (!signature.verify(signatureBytes)) {
throw new SignatureException("Signature did not verify");
}
} catch (InvalidKeyException e) {
throw new InvalidKeyException("Failed to verify generated " + jcaSignatureAlgorithm
+ " signature using public key from certificate", e);
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
| SignatureException e) {
throw new SignatureException("Failed to verify generated " + jcaSignatureAlgorithm
+ " signature using public key from certificate", e);
}
signer.signatures.add(Pair.create(signatureAlgorithm, signatureBytes));
}
// FORMAT:
// * length-prefixed signed data
// * length-prefixed sequence of length-prefixed signatures:
// * uint32: signature algorithm ID
// * length-prefixed bytes: signature of signed data
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
return encodeAsSequenceOfLengthPrefixedElements(
new byte[][] {
signer.signedData,
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
signer.signatures),
signer.publicKey,
});
}
private static final class V2SignatureSchemeBlock {
private static final class Signer {
public byte[] signedData;
public List<Pair<Integer, byte[]>> signatures;
public byte[] publicKey;
}
private static final class SignedData {
public List<Pair<Integer, byte[]>> digests;
public List<byte[]> certificates;
}
}
private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException {
byte[] encodedPublicKey = null;
if ("X.509".equals(publicKey.getFormat())) {
encodedPublicKey = publicKey.getEncoded();
}
if (encodedPublicKey == null) {
try {
encodedPublicKey =
KeyFactory.getInstance(publicKey.getAlgorithm())
.getKeySpec(publicKey, X509EncodedKeySpec.class)
.getEncoded();
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new InvalidKeyException(
"Failed to obtain X.509 encoded form of public key " + publicKey
+ " of class " + publicKey.getClass().getName(),
e);
}
}
if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) {
throw new InvalidKeyException(
"Failed to obtain X.509 encoded form of public key " + publicKey
+ " of class " + publicKey.getClass().getName());
}
return encodedPublicKey;
}
public static List<byte[]> encodeCertificates(List<X509Certificate> certificates)
throws CertificateEncodingException {
List<byte[]> result = new ArrayList<>();
for (X509Certificate certificate : certificates) {
result.add(certificate.getEncoded());
}
return result;
}
private static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) {
return encodeAsSequenceOfLengthPrefixedElements(
sequence.toArray(new byte[sequence.size()][]));
}
private static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) {
int payloadSize = 0;
for (byte[] element : sequence) {
payloadSize += 4 + element.length;
}
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
for (byte[] element : sequence) {
result.putInt(element.length);
result.put(element);
}
return result.array();
}
private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
List<Pair<Integer, byte[]>> sequence) {
int resultSize = 0;
for (Pair<Integer, byte[]> element : sequence) {
resultSize += 12 + element.getSecond().length;
}
ByteBuffer result = ByteBuffer.allocate(resultSize);
result.order(ByteOrder.LITTLE_ENDIAN);
for (Pair<Integer, byte[]> element : sequence) {
byte[] second = element.getSecond();
result.putInt(8 + second.length);
result.putInt(element.getFirst());
result.putInt(second.length);
result.put(second);
}
return result.array();
}
/**
* Relative <em>get</em> method for reading {@code size} number of bytes from the current
* position of this buffer.
*
* <p>This method reads the next {@code size} bytes at this buffer's current position,
* returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
* {@code size}, byte order set to this buffer's byte order; and then increments the position by
* {@code size}.
*/
private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
if (size < 0) {
throw new IllegalArgumentException("size: " + size);
}
int originalLimit = source.limit();
int position = source.position();
int limit = position + size;
if ((limit < position) || (limit > originalLimit)) {
throw new BufferUnderflowException();
}
source.limit(limit);
try {
ByteBuffer result = source.slice();
result.order(source.order());
source.position(limit);
return result;
} finally {
source.limit(originalLimit);
}
}
private static Pair<String, ? extends AlgorithmParameterSpec>
getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) {
switch (sigAlgorithm) {
case SIGNATURE_RSA_PSS_WITH_SHA256:
return Pair.create(
"SHA256withRSA/PSS",
new PSSParameterSpec(
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1));
case SIGNATURE_RSA_PSS_WITH_SHA512:
return Pair.create(
"SHA512withRSA/PSS",
new PSSParameterSpec(
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1));
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
return Pair.create("SHA256withRSA", null);
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
return Pair.create("SHA512withRSA", null);
case SIGNATURE_ECDSA_WITH_SHA256:
return Pair.create("SHA256withECDSA", null);
case SIGNATURE_ECDSA_WITH_SHA512:
return Pair.create("SHA512withECDSA", null);
case SIGNATURE_DSA_WITH_SHA256:
return Pair.create("SHA256withDSA", null);
case SIGNATURE_DSA_WITH_SHA512:
return Pair.create("SHA512withDSA", null);
default:
throw new IllegalArgumentException(
"Unknown signature algorithm: 0x"
+ Long.toHexString(sigAlgorithm & 0xffffffff));
}
}
private static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) {
switch (sigAlgorithm) {
case SIGNATURE_RSA_PSS_WITH_SHA256:
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
case SIGNATURE_ECDSA_WITH_SHA256:
case SIGNATURE_DSA_WITH_SHA256:
return CONTENT_DIGEST_CHUNKED_SHA256;
case SIGNATURE_RSA_PSS_WITH_SHA512:
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
case SIGNATURE_ECDSA_WITH_SHA512:
case SIGNATURE_DSA_WITH_SHA512:
return CONTENT_DIGEST_CHUNKED_SHA512;
default:
throw new IllegalArgumentException(
"Unknown signature algorithm: 0x"
+ Long.toHexString(sigAlgorithm & 0xffffffff));
}
}
private static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) {
switch (digestAlgorithm) {
case CONTENT_DIGEST_CHUNKED_SHA256:
return "SHA-256";
case CONTENT_DIGEST_CHUNKED_SHA512:
return "SHA-512";
default:
throw new IllegalArgumentException(
"Unknown content digest algorithm: " + digestAlgorithm);
}
}
private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) {
switch (digestAlgorithm) {
case CONTENT_DIGEST_CHUNKED_SHA256:
return 256 / 8;
case CONTENT_DIGEST_CHUNKED_SHA512:
return 512 / 8;
default:
throw new IllegalArgumentException(
"Unknown content digest algorithm: " + digestAlgorithm);
}
}
/**
* Indicates that APK file could not be parsed.
*/
public static class ApkParseException extends Exception {
private static final long serialVersionUID = 1L;
public ApkParseException(String message) {
super(message);
}
public ApkParseException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Pair of two elements.
*/
private static class Pair<A, B> {
private final A mFirst;
private final B mSecond;
private Pair(A first, B second) {
mFirst = first;
mSecond = second;
}
public static <A, B> Pair<A, B> create(A first, B second) {
return new Pair<>(first, second);
}
public A getFirst() {
return mFirst;
}
public B getSecond() {
return mSecond;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
@SuppressWarnings("rawtypes")
Pair other = (Pair) obj;
if (mFirst == null) {
if (other.mFirst != null) {
return false;
}
} else if (!mFirst.equals(other.mFirst)) {
return false;
}
if (mSecond == null) {
return other.mSecond == null;
} else return mSecond.equals(other.mSecond);
}
}
}

View file

@ -0,0 +1,35 @@
package com.topjohnwu.magisk.core.signing;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
public class ByteArrayStream extends ByteArrayOutputStream {
public synchronized void readFrom(InputStream is) {
readFrom(is, Integer.MAX_VALUE);
}
public synchronized void readFrom(InputStream is, int len) {
int read;
byte buffer[] = new byte[4096];
try {
while ((read = is.read(buffer, 0, Math.min(len, buffer.length))) > 0) {
write(buffer, 0, read);
len -= read;
}
} catch (IOException e) {
e.printStackTrace();
}
}
public ByteArrayInputStream getInputStream() {
return new ByteArrayInputStream(buf, 0, count);
}
public ByteBuffer toByteBuffer() {
return ByteBuffer.wrap(buf, 0, count);
}
}

View file

@ -0,0 +1,166 @@
package com.topjohnwu.magisk.core.signing;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public abstract class JarMap implements Closeable {
LinkedHashMap<String, JarEntry> entryMap;
public static JarMap open(File file, boolean verify) throws IOException {
return new FileMap(file, verify, ZipFile.OPEN_READ);
}
public static JarMap open(InputStream is, boolean verify) throws IOException {
return new StreamMap(is, verify);
}
public File getFile() {
return null;
}
public abstract Manifest getManifest() throws IOException;
public InputStream getInputStream(ZipEntry ze) throws IOException {
JarMapEntry e = getMapEntry(ze.getName());
return e != null ? e.data.getInputStream() : null;
}
public OutputStream getOutputStream(ZipEntry ze) {
if (entryMap == null)
entryMap = new LinkedHashMap<>();
JarMapEntry e = new JarMapEntry(ze.getName());
entryMap.put(ze.getName(), e);
return e.data;
}
public byte[] getRawData(ZipEntry ze) throws IOException {
JarMapEntry e = getMapEntry(ze.getName());
return e != null ? e.data.toByteArray() : null;
}
public abstract Enumeration<JarEntry> entries();
public final ZipEntry getEntry(String name) {
return getJarEntry(name);
}
public JarEntry getJarEntry(String name) {
return getMapEntry(name);
}
JarMapEntry getMapEntry(String name) {
JarMapEntry e = null;
if (entryMap != null)
e = (JarMapEntry) entryMap.get(name);
return e;
}
private static class FileMap extends JarMap {
private JarFile jarFile;
FileMap(File file, boolean verify, int mode) throws IOException {
jarFile = new JarFile(file, verify, mode);
}
@Override
public File getFile() {
return new File(jarFile.getName());
}
@Override
public Manifest getManifest() throws IOException {
return jarFile.getManifest();
}
@Override
public InputStream getInputStream(ZipEntry ze) throws IOException {
InputStream is = super.getInputStream(ze);
return is != null ? is : jarFile.getInputStream(ze);
}
@Override
public byte[] getRawData(ZipEntry ze) throws IOException {
byte[] b = super.getRawData(ze);
if (b != null)
return b;
ByteArrayStream bytes = new ByteArrayStream();
bytes.readFrom(jarFile.getInputStream(ze));
return bytes.toByteArray();
}
@Override
public Enumeration<JarEntry> entries() {
return jarFile.entries();
}
@Override
public JarEntry getJarEntry(String name) {
JarEntry e = getMapEntry(name);
return e != null ? e : jarFile.getJarEntry(name);
}
@Override
public void close() throws IOException {
jarFile.close();
}
}
private static class StreamMap extends JarMap {
private JarInputStream jis;
StreamMap(InputStream is, boolean verify) throws IOException {
jis = new JarInputStream(is, verify);
entryMap = new LinkedHashMap<>();
JarEntry entry;
while ((entry = jis.getNextJarEntry()) != null) {
entryMap.put(entry.getName(), new JarMapEntry(entry, jis));
}
}
@Override
public Manifest getManifest() {
return jis.getManifest();
}
@Override
public Enumeration<JarEntry> entries() {
return Collections.enumeration(entryMap.values());
}
@Override
public void close() throws IOException {
jis.close();
}
}
private static class JarMapEntry extends JarEntry {
ByteArrayStream data;
JarMapEntry(JarEntry je, InputStream is) {
super(je);
data = new ByteArrayStream();
data.readFrom(is);
}
JarMapEntry(String s) {
super(s);
data = new ByteArrayStream();
}
}
}

View file

@ -0,0 +1,566 @@
package com.topjohnwu.magisk.core.signing;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1OutputStream;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.encoders.Base64;
import java.io.ByteArrayOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.security.DigestOutputStream;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
/*
* Modified from AOSP
* https://android.googlesource.com/platform/build/+/refs/tags/android-7.1.2_r39/tools/signapk/src/com/android/signapk/SignApk.java
* */
public class SignApk {
private static final String CERT_SF_NAME = "META-INF/CERT.SF";
private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
// bitmasks for which hash algorithms we need the manifest to include.
private static final int USE_SHA1 = 1;
private static final int USE_SHA256 = 2;
/**
* Digest algorithm used when signing the APK using APK Signature Scheme v2.
*/
private static final String APK_SIG_SCHEME_V2_DIGEST_ALGORITHM = "SHA-256";
// Files matching this pattern are not copied to the output.
private static final Pattern stripPattern =
Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
/**
* Return one of USE_SHA1 or USE_SHA256 according to the signature
* algorithm specified in the cert.
*/
private static int getDigestAlgorithm(X509Certificate cert) {
String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) {
return USE_SHA1;
} else if (sigAlg.startsWith("SHA256WITH")) {
return USE_SHA256;
} else {
throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
"\" in cert [" + cert.getSubjectDN());
}
}
/**
* Returns the expected signature algorithm for this key type.
*/
private static String getSignatureAlgorithm(X509Certificate cert) {
String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
if ("RSA".equalsIgnoreCase(keyType)) {
if (getDigestAlgorithm(cert) == USE_SHA256) {
return "SHA256withRSA";
} else {
return "SHA1withRSA";
}
} else if ("EC".equalsIgnoreCase(keyType)) {
return "SHA256withECDSA";
} else {
throw new IllegalArgumentException("unsupported key type: " + keyType);
}
}
/**
* Add the hash(es) of every file to the manifest, creating it if
* necessary.
*/
private static Manifest addDigestsToManifest(JarMap jar, int hashes)
throws IOException, GeneralSecurityException {
Manifest input = jar.getManifest();
Manifest output = new Manifest();
Attributes main = output.getMainAttributes();
if (input != null) {
main.putAll(input.getMainAttributes());
} else {
main.putValue("Manifest-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
}
MessageDigest md_sha1 = null;
MessageDigest md_sha256 = null;
if ((hashes & USE_SHA1) != 0) {
md_sha1 = MessageDigest.getInstance("SHA1");
}
if ((hashes & USE_SHA256) != 0) {
md_sha256 = MessageDigest.getInstance("SHA256");
}
byte[] buffer = new byte[4096];
int num;
// We sort the input entries by name, and add them to the
// output manifest in sorted order. We expect that the output
// map will be deterministic.
TreeMap<String, JarEntry> byName = new TreeMap<>();
for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
JarEntry entry = e.nextElement();
byName.put(entry.getName(), entry);
}
for (JarEntry entry : byName.values()) {
String name = entry.getName();
if (!entry.isDirectory() && !stripPattern.matcher(name).matches()) {
InputStream data = jar.getInputStream(entry);
while ((num = data.read(buffer)) > 0) {
if (md_sha1 != null) md_sha1.update(buffer, 0, num);
if (md_sha256 != null) md_sha256.update(buffer, 0, num);
}
Attributes attr = null;
if (input != null) attr = input.getAttributes(name);
attr = attr != null ? new Attributes(attr) : new Attributes();
// Remove any previously computed digests from this entry's attributes.
for (Iterator<Object> i = attr.keySet().iterator(); i.hasNext(); ) {
Object key = i.next();
if (!(key instanceof Attributes.Name)) {
continue;
}
String attributeNameLowerCase =
key.toString().toLowerCase(Locale.US);
if (attributeNameLowerCase.endsWith("-digest")) {
i.remove();
}
}
// Add SHA-1 digest if requested
if (md_sha1 != null) {
attr.putValue("SHA1-Digest",
new String(Base64.encode(md_sha1.digest()), "ASCII"));
}
// Add SHA-256 digest if requested
if (md_sha256 != null) {
attr.putValue("SHA-256-Digest",
new String(Base64.encode(md_sha256.digest()), "ASCII"));
}
output.getEntries().put(name, attr);
}
}
return output;
}
/**
* Write a .SF file with a digest of the specified manifest.
*/
private static void writeSignatureFile(Manifest manifest, OutputStream out,
int hash)
throws IOException, GeneralSecurityException {
Manifest sf = new Manifest();
Attributes main = sf.getMainAttributes();
main.putValue("Signature-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
// Add APK Signature Scheme v2 signature stripping protection.
// This attribute indicates that this APK is supposed to have been signed using one or
// more APK-specific signature schemes in addition to the standard JAR signature scheme
// used by this code. APK signature verifier should reject the APK if it does not
// contain a signature for the signature scheme the verifier prefers out of this set.
main.putValue(
ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME,
ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE);
MessageDigest md = MessageDigest.getInstance(hash == USE_SHA256 ? "SHA256" : "SHA1");
PrintStream print = new PrintStream(new DigestOutputStream(new ByteArrayOutputStream(), md),
true, "UTF-8");
// Digest of the entire manifest
manifest.write(print);
print.flush();
main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
new String(Base64.encode(md.digest()), "ASCII"));
Map<String, Attributes> entries = manifest.getEntries();
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
// Digest of the manifest stanza for this entry.
print.print("Name: " + entry.getKey() + "\r\n");
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
}
print.print("\r\n");
print.flush();
Attributes sfAttr = new Attributes();
sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest",
new String(Base64.encode(md.digest()), "ASCII"));
sf.getEntries().put(entry.getKey(), sfAttr);
}
CountOutputStream cout = new CountOutputStream(out);
sf.write(cout);
// A bug in the java.util.jar implementation of Android platforms
// up to version 1.6 will cause a spurious IOException to be thrown
// if the length of the signature file is a multiple of 1024 bytes.
// As a workaround, add an extra CRLF in this case.
if ((cout.size() % 1024) == 0) {
cout.write('\r');
cout.write('\n');
}
}
/**
* Sign data and write the digital signature to 'out'.
*/
private static void writeSignatureBlock(
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, OutputStream out)
throws IOException,
CertificateEncodingException,
OperatorCreationException,
CMSException {
ArrayList<X509Certificate> certList = new ArrayList<>(1);
certList.add(publicKey);
JcaCertStore certs = new JcaCertStore(certList);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
.build(privateKey);
gen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build())
.setDirectSignature(true)
.build(signer, publicKey)
);
gen.addCertificates(certs);
CMSSignedData sigData = gen.generate(data, false);
try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
ASN1OutputStream dos = ASN1OutputStream.create(out, ASN1Encoding.DER);
dos.writeObject(asn1.readObject());
}
}
/**
* Copy all the files in a manifest from input to output. We set
* the modification times in the output to a fixed time, so as to
* reduce variation in the output file and make incremental OTAs
* more efficient.
*/
private static void copyFiles(Manifest manifest, JarMap in, JarOutputStream out,
long timestamp, int defaultAlignment) throws IOException {
byte[] buffer = new byte[4096];
int num;
Map<String, Attributes> entries = manifest.getEntries();
ArrayList<String> names = new ArrayList<>(entries.keySet());
Collections.sort(names);
boolean firstEntry = true;
long offset = 0L;
// We do the copy in two passes -- first copying all the
// entries that are STORED, then copying all the entries that
// have any other compression flag (which in practice means
// DEFLATED). This groups all the stored entries together at
// the start of the file and makes it easier to do alignment
// on them (since only stored entries are aligned).
for (String name : names) {
JarEntry inEntry = in.getJarEntry(name);
JarEntry outEntry;
if (inEntry.getMethod() != JarEntry.STORED) continue;
// Preserve the STORED method of the input entry.
outEntry = new JarEntry(inEntry);
outEntry.setTime(timestamp);
// Discard comment and extra fields of this entry to
// simplify alignment logic below and for consistency with
// how compressed entries are handled later.
outEntry.setComment(null);
outEntry.setExtra(null);
// 'offset' is the offset into the file at which we expect
// the file data to begin. This is the value we need to
// make a multiple of 'alignement'.
offset += JarFile.LOCHDR + outEntry.getName().length();
if (firstEntry) {
// The first entry in a jar file has an extra field of
// four bytes that you can't get rid of; any extra
// data you specify in the JarEntry is appended to
// these forced four bytes. This is JAR_MAGIC in
// JarOutputStream; the bytes are 0xfeca0000.
offset += 4;
firstEntry = false;
}
int alignment = getStoredEntryDataAlignment(name, defaultAlignment);
if (alignment > 0 && (offset % alignment != 0)) {
// Set the "extra data" of the entry to between 1 and
// alignment-1 bytes, to make the file data begin at
// an aligned offset.
int needed = alignment - (int) (offset % alignment);
outEntry.setExtra(new byte[needed]);
offset += needed;
}
out.putNextEntry(outEntry);
InputStream data = in.getInputStream(inEntry);
while ((num = data.read(buffer)) > 0) {
out.write(buffer, 0, num);
offset += num;
}
out.flush();
}
// Copy all the non-STORED entries. We don't attempt to
// maintain the 'offset' variable past this point; we don't do
// alignment on these entries.
for (String name : names) {
JarEntry inEntry = in.getJarEntry(name);
JarEntry outEntry;
if (inEntry.getMethod() == JarEntry.STORED) continue;
// Create a new entry so that the compressed len is recomputed.
outEntry = new JarEntry(name);
outEntry.setTime(timestamp);
out.putNextEntry(outEntry);
InputStream data = in.getInputStream(inEntry);
while ((num = data.read(buffer)) > 0) {
out.write(buffer, 0, num);
}
out.flush();
}
}
/**
* Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start
* relative to start of file or {@code 0} if alignment of this entry's data is not important.
*/
private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) {
if (defaultAlignment <= 0) {
return 0;
}
if (entryName.endsWith(".so")) {
// Align .so contents to memory page boundary to enable memory-mapped
// execution.
return 4096;
} else {
return defaultAlignment;
}
}
private static void signFile(Manifest manifest,
X509Certificate[] publicKey, PrivateKey[] privateKey,
long timestamp, JarOutputStream outputJar) throws Exception {
// MANIFEST.MF
JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
int numKeys = publicKey.length;
for (int k = 0; k < numKeys; ++k) {
// CERT.SF / CERT#.SF
je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
(String.format(Locale.US, CERT_SF_MULTI_NAME, k)));
je.setTime(timestamp);
outputJar.putNextEntry(je);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
byte[] signedData = baos.toByteArray();
outputJar.write(signedData);
// CERT.{EC,RSA} / CERT#.{EC,RSA}
final String keyType = publicKey[k].getPublicKey().getAlgorithm();
je = new JarEntry(numKeys == 1 ? (String.format(CERT_SIG_NAME, keyType)) :
(String.format(Locale.US, CERT_SIG_MULTI_NAME, k, keyType)));
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureBlock(new CMSProcessableByteArray(signedData),
publicKey[k], privateKey[k], outputJar);
}
}
/**
* Converts the provided lists of private keys, their X.509 certificates, and digest algorithms
* into a list of APK Signature Scheme v2 {@code SignerConfig} instances.
*/
private static List<ApkSignerV2.SignerConfig> createV2SignerConfigs(
PrivateKey[] privateKeys, X509Certificate[] certificates, String[] digestAlgorithms)
throws InvalidKeyException {
if (privateKeys.length != certificates.length) {
throw new IllegalArgumentException(
"The number of private keys must match the number of certificates: "
+ privateKeys.length + " vs" + certificates.length);
}
List<ApkSignerV2.SignerConfig> result = new ArrayList<>(privateKeys.length);
for (int i = 0; i < privateKeys.length; i++) {
PrivateKey privateKey = privateKeys[i];
X509Certificate certificate = certificates[i];
PublicKey publicKey = certificate.getPublicKey();
String keyAlgorithm = privateKey.getAlgorithm();
if (!keyAlgorithm.equalsIgnoreCase(publicKey.getAlgorithm())) {
throw new InvalidKeyException(
"Key algorithm of private key #" + (i + 1) + " does not match key"
+ " algorithm of public key #" + (i + 1) + ": " + keyAlgorithm
+ " vs " + publicKey.getAlgorithm());
}
ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig();
signerConfig.privateKey = privateKey;
signerConfig.certificates = Collections.singletonList(certificate);
List<Integer> signatureAlgorithms = new ArrayList<>(digestAlgorithms.length);
for (String digestAlgorithm : digestAlgorithms) {
try {
signatureAlgorithms.add(getV2SignatureAlgorithm(keyAlgorithm, digestAlgorithm));
} catch (IllegalArgumentException e) {
throw new InvalidKeyException(
"Unsupported key and digest algorithm combination for signer #"
+ (i + 1), e);
}
}
signerConfig.signatureAlgorithms = signatureAlgorithms;
result.add(signerConfig);
}
return result;
}
private static int getV2SignatureAlgorithm(String keyAlgorithm, String digestAlgorithm) {
if ("SHA-256".equalsIgnoreCase(digestAlgorithm)) {
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
// deterministic signatures which make life easier for OTA updates (fewer files
// changed when deterministic signature schemes are used).
return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256;
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA256;
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
return ApkSignerV2.SIGNATURE_DSA_WITH_SHA256;
} else {
throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
}
} else if ("SHA-512".equalsIgnoreCase(digestAlgorithm)) {
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
// deterministic signatures which make life easier for OTA updates (fewer files
// changed when deterministic signature schemes are used).
return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512;
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA512;
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
return ApkSignerV2.SIGNATURE_DSA_WITH_SHA512;
} else {
throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
}
} else {
throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
}
}
public static void sign(X509Certificate cert, PrivateKey key,
JarMap inputJar, OutputStream outputStream) throws Exception {
int alignment = 4;
int hashes = 0;
X509Certificate[] publicKey = new X509Certificate[1];
publicKey[0] = cert;
hashes |= getDigestAlgorithm(publicKey[0]);
// Set all ZIP file timestamps to Jan 1 2009 00:00:00.
long timestamp = 1230768000000L;
// The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS
// timestamp using the current timezone. We thus adjust the milliseconds since epoch
// value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00.
timestamp -= TimeZone.getDefault().getOffset(timestamp);
PrivateKey[] privateKey = new PrivateKey[1];
privateKey[0] = key;
// Generate, in memory, an APK signed using standard JAR Signature Scheme.
ByteArrayStream v1SignedApkBuf = new ByteArrayStream();
JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
// Use maximum compression for compressed entries because the APK lives forever on
// the system partition.
outputJar.setLevel(9);
Manifest manifest = addDigestsToManifest(inputJar, hashes);
copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
signFile(manifest, publicKey, privateKey, timestamp, outputJar);
outputJar.close();
ByteBuffer v1SignedApk = v1SignedApkBuf.toByteBuffer();
ByteBuffer[] outputChunks;
List<ApkSignerV2.SignerConfig> signerConfigs = createV2SignerConfigs(privateKey, publicKey,
new String[]{APK_SIG_SCHEME_V2_DIGEST_ALGORITHM});
outputChunks = ApkSignerV2.sign(v1SignedApk, signerConfigs);
// This assumes outputChunks are array-backed. To avoid this assumption, the
// code could be rewritten to use FileChannel.
for (ByteBuffer outputChunk : outputChunks) {
outputStream.write(outputChunk.array(),
outputChunk.arrayOffset() + outputChunk.position(), outputChunk.remaining());
outputChunk.position(outputChunk.limit());
}
}
/**
* Write to another stream and track how many bytes have been
* written.
*/
private static class CountOutputStream extends FilterOutputStream {
private int mCount;
public CountOutputStream(OutputStream out) {
super(out);
mCount = 0;
}
@Override
public void write(int b) throws IOException {
super.write(b);
mCount++;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
super.write(b, off, len);
mCount += len;
}
public int size() {
return mCount;
}
}
}

View file

@ -0,0 +1,136 @@
package com.topjohnwu.magisk.core.signing;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Assorted ZIP format helpers.
*
* <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
* order of these buffers is little-endian.
*/
public abstract class ZipUtils {
private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50;
private static final int UINT16_MAX_VALUE = 0xffff;
private ZipUtils() {
}
/**
* Returns the position at which ZIP End of Central Directory record starts in the provided
* buffer or {@code -1} if the record is not present.
*
* <p>NOTE: Byte order of {@code zipContents} must be little-endian.
*/
public static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
assertByteOrderLittleEndian(zipContents);
// ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
// The record can be identified by its 4-byte signature/magic which is located at the very
// beginning of the record. A complication is that the record is variable-length because of
// the comment field.
// The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
// end of the buffer for the EOCD record signature. Whenever we find a signature, we check
// the candidate record's comment length is such that the remainder of the record takes up
// exactly the remaining bytes in the buffer. The search is bounded because the maximum
// size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
int archiveSize = zipContents.capacity();
if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
return -1;
}
int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength; expectedCommentLength++) {
int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
int actualCommentLength = getUnsignedInt16(zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
if (actualCommentLength == expectedCommentLength) {
return eocdStartPos;
}
}
}
return -1;
}
/**
* Returns {@code true} if the provided buffer contains a ZIP64 End of Central Directory
* Locator.
*
* <p>NOTE: Byte order of {@code zipContents} must be little-endian.
*/
public static boolean isZip64EndOfCentralDirectoryLocatorPresent(ByteBuffer zipContents, int zipEndOfCentralDirectoryPosition) {
assertByteOrderLittleEndian(zipContents);
// ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
// Directory Record.
int locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
if (locatorPosition < 0) {
return false;
}
return zipContents.getInt(locatorPosition) == ZIP64_EOCD_LOCATOR_SIG;
}
/**
* Returns the offset of the start of the ZIP Central Directory in the archive.
*
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
*/
public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
return getUnsignedInt32(zipEndOfCentralDirectory, zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
}
/**
* Sets the offset of the start of the ZIP Central Directory in the archive.
*
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
*/
public static void setZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory, long offset) {
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
setUnsignedInt32(zipEndOfCentralDirectory, zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET, offset);
}
/**
* Returns the size (in bytes) of the ZIP Central Directory.
*
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
*/
public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
return getUnsignedInt32(zipEndOfCentralDirectory, zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
}
private static void assertByteOrderLittleEndian(ByteBuffer buffer) {
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
}
}
private static int getUnsignedInt16(ByteBuffer buffer, int offset) {
return buffer.getShort(offset) & 0xffff;
}
private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
return buffer.getInt(offset) & 0xffffffffL;
}
private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
if ((value < 0) || (value > 0xffffffffL)) {
throw new IllegalArgumentException("uint32 value of out range: " + value);
}
buffer.putInt(buffer.position() + offset, (int) value);
}
}

View file

@ -0,0 +1,102 @@
package com.topjohnwu.magisk.core.su
import android.content.Context
import android.os.Bundle
import android.widget.Toast
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.ktx.getLabel
import com.topjohnwu.magisk.core.ktx.getPackageInfo
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.core.model.su.createSuLog
import kotlinx.coroutines.runBlocking
import timber.log.Timber
object SuCallbackHandler {
const val REQUEST = "request"
const val LOG = "log"
const val NOTIFY = "notify"
fun run(context: Context, action: String?, data: Bundle?) {
data ?: return
// Debug messages
if (BuildConfig.DEBUG) {
Timber.d(action)
data.let { bundle ->
bundle.keySet().forEach {
Timber.d("[%s]=[%s]", it, bundle[it])
}
}
}
when (action) {
LOG -> handleLogging(context, data)
NOTIFY -> handleNotify(context, data)
}
}
// https://android.googlesource.com/platform/frameworks/base/+/547bf5487d52b93c9fe183aa6d56459c170b17a4
private fun Bundle.getIntComp(key: String, defaultValue: Int): Int {
val value = get(key) ?: return defaultValue
return when (value) {
is Int -> value
is Long -> value.toInt()
else -> defaultValue
}
}
private fun handleLogging(context: Context, data: Bundle) {
val fromUid = data.getIntComp("from.uid", -1)
val notify = data.getBoolean("notify", true)
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
val toUid = data.getIntComp("to.uid", -1)
val pid = data.getIntComp("pid", -1)
val command = data.getString("command", "")
val target = data.getIntComp("target", -1)
val seContext = data.getString("context", "")
val gids = data.getString("gids", "")
val pm = context.packageManager
val log = runCatching {
pm.getPackageInfo(fromUid, pid)?.applicationInfo?.let {
pm.createSuLog(it, toUid, pid, command, policy, target, seContext, gids)
}
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy, target, seContext, gids)
if (notify)
notify(context, log.action >= SuPolicy.ALLOW, log.appName)
runBlocking { ServiceLocator.logRepo.insert(log) }
}
private fun handleNotify(context: Context, data: Bundle) {
val uid = data.getIntComp("from.uid", -1)
val pid = data.getIntComp("pid", -1)
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
val pm = context.packageManager
val appName = runCatching {
pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm)
}.getOrNull() ?: "[UID] $uid"
notify(context, policy >= SuPolicy.ALLOW, appName)
}
private fun notify(context: Context, granted: Boolean, appName: String) {
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
val resId = if (granted)
R.string.su_allow_toast
else
R.string.su_deny_toast
context.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
}
}
}

View file

@ -0,0 +1,110 @@
package com.topjohnwu.magisk.core.su
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.ktx.getPackageInfo
import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.DataOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.concurrent.TimeUnit
class SuRequestHandler(
val pm: PackageManager,
private val policyDB: PolicyDao
) {
private lateinit var output: File
private lateinit var policy: SuPolicy
lateinit var pkgInfo: PackageInfo
private set
// Return true to indicate undetermined policy, require user interaction
suspend fun start(intent: Intent): Boolean {
if (!init(intent))
return false
// Never allow com.topjohnwu.magisk (could be malware)
if (pkgInfo.packageName == BuildConfig.APP_PACKAGE_NAME) {
Shell.cmd("(pm uninstall ${BuildConfig.APP_PACKAGE_NAME} >/dev/null 2>&1)&").exec()
return false
}
when (Config.suAutoResponse) {
Config.Value.SU_AUTO_DENY -> {
respond(SuPolicy.DENY, 0)
return false
}
Config.Value.SU_AUTO_ALLOW -> {
respond(SuPolicy.ALLOW, 0)
return false
}
}
return true
}
private suspend fun init(intent: Intent): Boolean {
val uid = intent.getIntExtra("uid", -1)
val pid = intent.getIntExtra("pid", -1)
val fifo = intent.getStringExtra("fifo")
if (uid <= 0 || pid <= 0 || fifo == null) {
Timber.e("Unexpected extras: uid=[${uid}], pid=[${pid}], fifo=[${fifo}]")
return false
}
output = File(fifo)
policy = policyDB.fetch(uid) ?: SuPolicy(uid)
try {
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()
// We only fill in sharedUserId and leave other fields uninitialized
sharedUserId = name.split(":")[0]
}
} catch (e: PackageManager.NameNotFoundException) {
Timber.e(e)
respond(SuPolicy.DENY, -1)
return false
}
if (!output.canWrite()) {
Timber.e("Cannot write to $output")
return false
}
return true
}
suspend fun respond(action: Int, time: Long) {
if (action == SuPolicy.ALLOW && Config.suRestrict) {
policy.policy = SuPolicy.RESTRICT
} else {
policy.policy = action
}
if (time >= 0) {
policy.remain = TimeUnit.MINUTES.toSeconds(time)
} else {
policy.remain = time
}
withContext(Dispatchers.IO) {
try {
DataOutputStream(FileOutputStream(output)).use {
it.writeInt(policy.policy)
it.flush()
}
} catch (e: IOException) {
Timber.e(e)
}
if (time >= 0) {
policyDB.update(policy)
}
}
}
}

View file

@ -0,0 +1,296 @@
package com.topjohnwu.magisk.core.tasks
import android.app.Activity
import android.app.ActivityOptions
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.widget.Toast
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.AppApkPath
import com.topjohnwu.magisk.core.BuildConfig.APP_PACKAGE_NAME
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.ktx.await
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.signing.JarMap
import com.topjohnwu.magisk.core.signing.SignApk
import com.topjohnwu.magisk.core.utils.AXML
import com.topjohnwu.magisk.core.utils.Keygen
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.io.OutputStream
import java.security.SecureRandom
import kotlin.random.asKotlinRandom
object AppMigration {
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
private const val ALPHADOTS = "$ALPHA....."
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
private const val TEST_PKG_NAME = "$APP_PACKAGE_NAME.test"
// Some arbitrary limit
const val MAX_LABEL_LENGTH = 32
const val PLACEHOLDER = "COMPONENT_PLACEHOLDER"
private fun genPackageName(): String {
val random = SecureRandom()
val len = 5 + random.nextInt(15)
val builder = StringBuilder(len)
var next: Char
var prev = 0.toChar()
for (i in 0 until len) {
next = if (prev == '.' || i == 0 || i == len - 1) {
ALPHA[random.nextInt(ALPHA.length)]
} else {
ALPHADOTS[random.nextInt(ALPHADOTS.length)]
}
builder.append(next)
prev = next
}
if (!builder.contains('.')) {
// Pick a random index and set it as dot
val idx = random.nextInt(len - 2)
builder[idx + 1] = '.'
}
return builder.toString()
}
private fun classNameGenerator() = sequence {
val c1 = mutableListOf<String>()
val c2 = mutableListOf<String>()
val c3 = mutableListOf<String>()
val random = SecureRandom()
val kRandom = random.asKotlinRandom()
fun <T> chain(vararg iters: Iterable<T>) = sequence {
iters.forEach { it.forEach { v -> yield(v) } }
}
for (a in chain('a'..'z', 'A'..'Z')) {
if (a != 'a' && a != 'A') {
c1.add("$a")
}
for (b in chain('a'..'z', 'A'..'Z', '0'..'9')) {
c2.add("$a$b")
for (c in chain('a'..'z', 'A'..'Z', '0'..'9')) {
c3.add("$a$b$c")
}
}
}
c1.shuffle(random)
c2.shuffle(random)
c3.shuffle(random)
fun notJavaKeyword(name: String) = when (name) {
"do", "if", "for", "int", "new", "try" -> false
else -> true
}
fun List<String>.process() = asSequence().filter(::notJavaKeyword)
val names = mutableListOf<String>()
names.addAll(c1)
names.addAll(c2.process().take(30))
names.addAll(c3.process().take(30))
while (true) {
val seg = 2 + random.nextInt(4)
val cls = StringBuilder()
for (i in 0 until seg) {
cls.append(names.random(kRandom))
if (i != seg - 1)
cls.append('.')
}
// Old Android does not support capitalized package names
// Check Android 7.0.0 PackageParser#buildClassName
cls[0] = cls[0].lowercaseChar()
yield(cls.toString())
}
}.distinct().iterator()
private fun patch(
context: Context,
apk: File, out: OutputStream,
pkg: String, label: CharSequence
): Boolean {
val pm = context.packageManager
val info = pm.getPackageArchiveInfo(apk.path, 0)?.applicationInfo ?: return false
val origLabel = info.nonLocalizedLabel.toString()
try {
JarMap.open(apk, true).use { jar ->
val je = jar.getJarEntry(ANDROID_MANIFEST)
val xml = AXML(jar.getRawData(je))
val generator = classNameGenerator()
val p = xml.patchStrings {
when {
it.contains(APP_PACKAGE_NAME) -> it.replace(APP_PACKAGE_NAME, pkg)
it.contains(PLACEHOLDER) -> generator.next()
it == origLabel -> label.toString()
else -> it
}
}
if (!p) return false
// Write apk changes
jar.getOutputStream(je).use { it.write(xml.bytes) }
val keys = Keygen()
SignApk.sign(keys.cert, keys.key, jar, out)
return true
}
} catch (e: Exception) {
Timber.e(e)
return false
}
}
private fun patchTest(apk: File, out: File, pkg: String): Boolean {
try {
JarMap.open(apk, true).use { jar ->
val je = jar.getJarEntry(ANDROID_MANIFEST)
val xml = AXML(jar.getRawData(je))
val p = xml.patchStrings {
when (it) {
APP_PACKAGE_NAME -> pkg
TEST_PKG_NAME -> "$pkg.test"
else -> it
}
}
if (!p) return false
// Write apk changes
jar.getOutputStream(je).use { it.write(xml.bytes) }
val keys = Keygen()
out.outputStream().use { SignApk.sign(keys.cert, keys.key, jar, it) }
return true
}
} catch (e: Exception) {
Timber.e(e)
return false
}
}
private fun launchApp(context: Context, pkg: String) {
val intent = context.packageManager.getLaunchIntentForPackage(pkg) ?: return
intent.putExtra(Const.Key.PREV_CONFIG, Config.toBundle())
val options = ActivityOptions.makeBasic()
if (Build.VERSION.SDK_INT >= 34) {
options.setShareIdentityEnabled(true)
}
context.startActivity(intent, options.toBundle())
if (context is Activity) {
context.finish()
}
}
suspend fun patchAndHide(context: Context, label: String, pkg: String? = null): Boolean {
val stub = File(context.cacheDir, "stub.apk")
try {
context.assets.open("stub.apk").writeTo(stub)
} catch (e: IOException) {
Timber.e(e)
return false
}
// Generate a new random signature and package name if needed
val pkg = pkg ?: genPackageName()
Config.keyStoreRaw = ""
// Check and patch the test APK
try {
val info = context.packageManager.getApplicationInfo(TEST_PKG_NAME, 0)
val testApk = File(info.sourceDir)
val testRepack = File(context.cacheDir, "test.apk")
if (!patchTest(testApk, testRepack, pkg))
return false
val cmd = "adb_pm_install $testRepack $pkg.test"
if (!Shell.cmd(cmd).exec().isSuccess)
return false
} catch (e: PackageManager.NameNotFoundException) {
}
val repack = File(context.cacheDir, "patched.apk")
repack.outputStream().use {
if (!patch(context, stub, it, pkg, label))
return false
}
// Install and auto launch app
val cmd = "adb_pm_install $repack $pkg"
if (Shell.cmd(cmd).exec().isSuccess) {
Config.suManager = pkg
Shell.cmd("touch $AppApkPath").exec()
launchApp(context, pkg)
return true
} else {
return false
}
}
@Suppress("DEPRECATION")
suspend fun hide(activity: Activity, label: String) {
val dialog = android.app.ProgressDialog(activity).apply {
setTitle(activity.getString(R.string.hide_app_title))
isIndeterminate = true
setCancelable(false)
show()
}
val success = withContext(Dispatchers.IO) {
patchAndHide(activity, label)
}
if (!success) {
dialog.dismiss()
activity.toast(R.string.failure, Toast.LENGTH_LONG)
}
}
suspend fun restoreApp(context: Context): Boolean {
val apk = StubApk.current(context)
val cmd = "adb_pm_install $apk $APP_PACKAGE_NAME"
if (Shell.cmd(cmd).await().isSuccess) {
Config.suManager = ""
Shell.cmd("touch $AppApkPath").exec()
launchApp(context, APP_PACKAGE_NAME)
return true
}
return false
}
@Suppress("DEPRECATION")
suspend fun restore(activity: Activity) {
val dialog = android.app.ProgressDialog(activity).apply {
setTitle(activity.getString(R.string.restore_img_msg))
isIndeterminate = true
setCancelable(false)
show()
}
if (!restoreApp(activity)) {
activity.toast(R.string.failure, Toast.LENGTH_LONG)
}
dialog.dismiss()
}
suspend fun upgradeStub(context: Context, apk: File): Intent? {
val label = context.applicationInfo.nonLocalizedLabel
val pkg = context.packageName
val session = APKInstall.startSession(context)
return withContext(Dispatchers.IO) {
session.openStream(context).use {
if (!patch(context, apk, it, pkg, label)) {
return@withContext null
}
}
session.waitIntent()
}
}
}

View file

@ -0,0 +1,78 @@
package com.topjohnwu.magisk.core.tasks
import android.net.Uri
import androidx.core.net.toFile
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
open class FlashZip(
private val mUri: Uri,
private val console: MutableList<String>,
private val logs: MutableList<String>
) {
private val installDir = File(AppContext.cacheDir, "flash")
private lateinit var zipFile: File
@Throws(IOException::class)
private suspend fun flash(): Boolean {
installDir.deleteRecursively()
installDir.mkdirs()
zipFile = if (mUri.scheme == "file") {
mUri.toFile()
} else {
File(installDir, "install.zip").also {
console.add("- Copying zip to temp directory")
try {
mUri.inputStream().writeTo(it)
} catch (e: IOException) {
when (e) {
is FileNotFoundException -> console.add("! Invalid Uri")
else -> console.add("! Cannot copy to cache")
}
throw e
}
}
}
try {
val binary = File(installDir, "update-binary")
AppContext.assets.open("module_installer.sh").use { it.writeTo(binary) }
} catch (e: IOException) {
console.add("! Unzip error")
throw e
}
console.add("- Installing ${mUri.displayName}")
return Shell.cmd("sh $installDir/update-binary dummy 1 \'$zipFile\'")
.to(console, logs).exec().isSuccess
}
open suspend fun exec() = withContext(Dispatchers.IO) {
try {
if (!flash()) {
console.add("! Installation failed")
false
} else {
true
}
} catch (e: IOException) {
Timber.e(e)
false
} finally {
Shell.cmd("cd /", "rm -rf $installDir ${Const.TMPDIR}").submit()
}
}
}

View file

@ -0,0 +1,696 @@
package com.topjohnwu.magisk.core.tasks
import android.net.Uri
import android.os.Process
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import android.system.OsConstants.O_WRONLY
import androidx.annotation.WorkerThread
import androidx.core.os.postDelayed
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.AppApkPath
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.copyAll
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.utils.DummyList
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.superuser.nio.ExtendedFile
import com.topjohnwu.superuser.nio.FileSystemManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.commons.compress.archivers.tar.TarArchiveEntry
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream
import org.apache.commons.compress.archivers.zip.ZipFile
import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorInputStream
import timber.log.Timber
import java.io.File
import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.PushbackInputStream
import java.nio.ByteBuffer
import java.security.SecureRandom
import java.util.Locale
import java.util.concurrent.atomic.AtomicBoolean
abstract class MagiskInstallImpl protected constructor(
protected val console: MutableList<String>,
private val logs: MutableList<String>
) {
private lateinit var installDir: ExtendedFile
private lateinit var srcBoot: ExtendedFile
private val shell = Shell.getShell()
private val useRootDir = shell.isRoot && Info.noDataExec
protected val context get() = ServiceLocator.deContext
private val rootFS get() = RootUtils.fs
private val localFS get() = FileSystemManager.getLocal()
private val destName: String by lazy {
if (Config.randName) {
val alpha = "abcdefghijklmnopqrstuvwxyz"
val alphaNum = "$alpha${alpha.uppercase(Locale.ROOT)}0123456789"
val random = SecureRandom()
StringBuilder("magisk_patched-${BuildConfig.APP_VERSION_CODE}_").run {
for (i in 1..5) {
append(alphaNum[random.nextInt(alphaNum.length)])
}
toString()
}
} else {
"magisk_patched"
}
}
private fun findImage(slot: String): Boolean {
val cmd =
"RECOVERYMODE=${Config.recovery} " +
"VENDORBOOT=${Info.isVendorBoot} " +
"SLOT=$slot " +
"find_boot_image; echo \$BOOTIMAGE"
val bootPath = ("($cmd)").fsh()
if (bootPath.isEmpty()) {
console.add("! Unable to detect target image")
return false
}
srcBoot = rootFS.getFile(bootPath)
console.add("- Target image: $bootPath")
return true
}
private fun findImage(): Boolean {
return findImage(Info.slot)
}
private fun findSecondary(): Boolean {
val slot = if (Info.slot == "_a") "_b" else "_a"
console.add("- Target slot: $slot")
return findImage(slot)
}
private suspend fun extractFiles(): Boolean {
console.add("- Device platform: ${Const.CPU_ABI}")
console.add("- Installing: ${BuildConfig.APP_VERSION_NAME} (${BuildConfig.APP_VERSION_CODE})")
installDir = localFS.getFile(context.filesDir.parent, "install")
installDir.deleteRecursively()
installDir.mkdirs()
try {
// Extract binaries
if (isRunningAsStub) {
ZipFile.builder().setFile(StubApk.current(context)).get().use { zf ->
zf.entries.asSequence().filter {
!it.isDirectory && it.name.startsWith("lib/${Const.CPU_ABI}/")
}.forEach {
val n = it.name.substring(it.name.lastIndexOf('/') + 1)
val name = n.substring(3, n.length - 3)
val dest = File(installDir, name)
zf.getInputStream(it).writeTo(dest)
dest.setExecutable(true)
}
val abi32 = Const.CPU_ABI_32
if (Process.is64Bit() && abi32 != null) {
val entry = zf.getEntry("lib/$abi32/libmagisk.so")
if (entry != null) {
val magisk32 = File(installDir, "magisk32")
zf.getInputStream(entry).writeTo(magisk32)
}
}
}
} else {
val info = context.applicationInfo
val libs = File(info.nativeLibraryDir).listFiles { _, name ->
name.startsWith("lib") && name.endsWith(".so")
} ?: emptyArray()
for (lib in libs) {
val name = lib.name.substring(3, lib.name.length - 3)
Os.symlink(lib.path, "$installDir/$name")
}
// Also extract magisk32 on 64-bit devices that supports 32-bit
val abi32 = Const.CPU_ABI_32
if (Process.is64Bit() && abi32 != null) {
val name = "lib/$abi32/libmagisk.so"
val entry = javaClass.classLoader!!.getResourceAsStream(name)
if (entry != null) {
val magisk32 = File(installDir, "magisk32")
entry.writeTo(magisk32)
}
}
}
// Extract scripts
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh", "stub.apk")) {
val dest = File(installDir, script)
context.assets.open(script).writeTo(dest)
}
// Extract chromeos tools
File(installDir, "chromeos").mkdir()
for (file in listOf("futility", "kernel_data_key.vbprivk", "kernel.keyblock")) {
val name = "chromeos/$file"
val dest = File(installDir, name)
context.assets.open(name).writeTo(dest)
}
} catch (e: Exception) {
console.add("! Unable to extract files")
Timber.e(e)
return false
}
if (useRootDir) {
// Move everything to tmpfs to workaround Samsung bullshit
rootFS.getFile(Const.TMPDIR).also {
arrayOf(
"rm -rf $it",
"mkdir -p $it",
"cp_readlink $installDir $it",
"rm -rf $installDir"
).sh()
installDir = it
}
}
return true
}
private suspend fun InputStream.copyAndCloseOut(out: OutputStream) =
out.use { copyAll(it, 1024 * 1024) }
private class NoAvailableStream(s: InputStream) : FilterInputStream(s) {
// Make sure available is never called on the actual stream and always return 0
// to reduce max buffer size and avoid OOM
override fun available() = 0
}
private class NoBootException : IOException()
inner class BootItem(private val entry: TarArchiveEntry) {
val name = entry.name.replace(".lz4", "")
var file = installDir.getChildFile(name)
suspend fun copyTo(tarOut: TarArchiveOutputStream) {
entry.name = name
entry.size = file.length()
file.newInputStream().use {
console.add("-- Writing : $name")
tarOut.putArchiveEntry(entry)
it.copyAll(tarOut)
tarOut.closeArchiveEntry()
}
}
}
@Throws(IOException::class)
private suspend fun processTar(
tarIn: TarArchiveInputStream,
tarOut: TarArchiveOutputStream
): BootItem {
console.add("- Processing tar file")
var entry: TarArchiveEntry? = tarIn.nextEntry
fun decompressedStream(): InputStream {
val stream = if (tarIn.currentEntry.name.endsWith(".lz4"))
FramedLZ4CompressorInputStream(tarIn, true) else tarIn
return NoAvailableStream(stream)
}
var boot: BootItem? = null
var initBoot: BootItem? = null
var recovery: BootItem? = null
while (entry != null) {
val bootItem: BootItem?
if (entry.name.startsWith("boot.img")) {
bootItem = BootItem(entry)
boot = bootItem
} else if (entry.name.startsWith("init_boot.img")) {
bootItem = BootItem(entry)
initBoot = bootItem
} else if (Config.recovery && entry.name.contains("recovery.img")) {
bootItem = BootItem(entry)
recovery = bootItem
} else {
bootItem = null
}
if (bootItem != null) {
console.add("-- Extracting: ${bootItem.name}")
decompressedStream().copyAndCloseOut(bootItem.file.newOutputStream())
} else if (entry.name.contains("vbmeta.img")) {
val rawData = decompressedStream().readBytes()
// Valid vbmeta.img should be at least 256 bytes
if (rawData.size < 256)
continue
// vbmeta partition exist, disable boot vbmeta patch
Info.patchBootVbmeta = false
val name = entry.name.replace(".lz4", "")
console.add("-- Patching : $name")
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
ByteBuffer.wrap(rawData).putInt(120, 3)
// Fetch the next entry first before modifying current entry
val vbmeta = entry
entry = tarIn.nextEntry
// Update entry with new information
vbmeta.name = name
vbmeta.size = rawData.size.toLong()
// Write output
tarOut.putArchiveEntry(vbmeta)
tarOut.write(rawData)
tarOut.closeArchiveEntry()
continue
} else if (entry.name.contains("userdata.img")) {
console.add("-- Skipping : ${entry.name}")
} else {
console.add("-- Copying : ${entry.name}")
tarOut.putArchiveEntry(entry)
tarIn.copyAll(tarOut)
tarOut.closeArchiveEntry()
}
entry = tarIn.nextEntry ?: break
}
// Patch priority: recovery > init_boot > boot
return when {
recovery != null -> {
if (boot != null) {
// Repack boot image to prevent auto restore
arrayOf(
"cd $installDir",
"chmod -R 755 .",
"./magiskboot unpack boot.img",
"./magiskboot repack boot.img",
"cat new-boot.img > boot.img",
"./magiskboot cleanup",
"rm -f new-boot.img",
"cd /").sh()
boot.copyTo(tarOut)
}
recovery
}
initBoot != null -> {
boot?.copyTo(tarOut)
initBoot
}
boot != null -> boot
else -> throw NoBootException()
}
}
@Throws(IOException::class)
private suspend fun processZip(zipIn: ZipArchiveInputStream): ExtendedFile {
console.add("- Processing zip file")
val boot = installDir.getChildFile("boot.img")
val initBoot = installDir.getChildFile("init_boot.img")
var entry: ZipArchiveEntry
while (true) {
entry = zipIn.nextEntry ?: break
if (entry.isDirectory) continue
when (entry.name.substringAfterLast('/')) {
"payload.bin" -> {
try {
return processPayload(zipIn)
} catch (e: IOException) {
// No boot image in payload.bin, continue to find boot images
}
}
"init_boot.img" -> {
console.add("- Extracting init_boot.img")
zipIn.copyAndCloseOut(initBoot.newOutputStream())
return initBoot
}
"boot.img" -> {
console.add("- Extracting boot.img")
zipIn.copyAndCloseOut(boot.newOutputStream())
// Don't return here since there might be an init_boot.img
}
}
}
if (boot.exists()) {
return boot
} else {
throw NoBootException()
}
}
@Throws(IOException::class)
private fun processPayload(input: InputStream): ExtendedFile {
var fifo: File? = null
try {
console.add("- Processing payload.bin")
fifo = File.createTempFile("payload-fifo-", null, installDir)
fifo.delete()
Os.mkfifo(fifo.path, 420 /* 0644 */)
// Enqueue the shell command first, or the subsequent FIFO open will block
val future = arrayOf(
"cd $installDir",
"./magiskboot extract $fifo",
"cd /"
).eq()
val fd = Os.open(fifo.path, O_WRONLY, 0)
try {
val bufSize = 1024 * 1024
val buf = ByteBuffer.allocate(bufSize)
buf.position(input.read(buf.array()).coerceAtLeast(0)).flip()
while (buf.hasRemaining()) {
try {
Os.write(fd, buf)
} catch (e: ErrnoException) {
if (e.errno != OsConstants.EPIPE)
throw e
// If SIGPIPE, then the other side is closed, we're done
break
}
if (!buf.hasRemaining()) {
buf.limit(bufSize)
buf.position(input.read(buf.array()).coerceAtLeast(0)).flip()
}
}
} finally {
Os.close(fd)
}
val success = try { future.get().isSuccess } catch (e: Exception) { false }
if (!success) {
console.add("! Error while extracting payload.bin")
throw IOException()
}
val boot = installDir.getChildFile("boot.img")
val initBoot = installDir.getChildFile("init_boot.img")
return when {
initBoot.exists() -> {
console.add("-- Extract init_boot.img")
initBoot
}
boot.exists() -> {
console.add("-- Extract boot.img")
boot
}
else -> {
throw NoBootException()
}
}
} catch (e: ErrnoException) {
throw IOException(e)
} finally {
fifo?.delete()
}
}
private suspend fun processFile(uri: Uri): Boolean {
val outStream: OutputStream
val outFile: MediaStoreUtils.UriFile
var bootItem: BootItem? = null
// Process input file
try {
PushbackInputStream(uri.inputStream().buffered(1024 * 1024), 512).use { src ->
val head = ByteArray(512)
if (src.read(head) != head.size) {
console.add("! Invalid input file")
return false
}
src.unread(head)
val magic = head.copyOf(4)
val tarMagic = head.copyOfRange(257, 262)
srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) {
// tar file
outFile = MediaStoreUtils.getFile("$destName.tar")
val os = outFile.uri.outputStream().buffered(1024 * 1024)
outStream = TarArchiveOutputStream(os).also {
it.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR)
it.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU)
}
try {
bootItem = processTar(TarArchiveInputStream(src), outStream)
bootItem.file
} catch (e: IOException) {
outStream.close()
outFile.delete()
throw e
}
} else {
// raw image
outFile = MediaStoreUtils.getFile("$destName.img")
outStream = outFile.uri.outputStream()
try {
if (magic.contentEquals("CrAU".toByteArray())) {
processPayload(src)
} else if (magic.contentEquals("PK\u0003\u0004".toByteArray())) {
processZip(ZipArchiveInputStream(src))
} else {
console.add("- Copying image to cache")
installDir.getChildFile("boot.img").also {
src.copyAndCloseOut(it.newOutputStream())
}
}
} catch (e: IOException) {
outStream.close()
outFile.delete()
throw e
}
}
}
} catch (e: IOException) {
if (e is NoBootException)
console.add("! No boot image found")
console.add("! Process error")
Timber.e(e)
return false
}
// Patch file
if (!patchBoot()) {
outFile.delete()
return false
}
// Output file
try {
val newBoot = installDir.getChildFile("new-boot.img")
if (bootItem != null) {
bootItem.file = newBoot
bootItem.copyTo(outStream as TarArchiveOutputStream)
} else {
newBoot.newInputStream().use { it.copyAll(outStream, 1024 * 1024) }
}
newBoot.delete()
console.add("")
console.add("****************************")
console.add(" Output file is written to ")
console.add(" $outFile ")
console.add("****************************")
} catch (e: IOException) {
console.add("! Failed to output to $outFile")
outFile.delete()
Timber.e(e)
return false
} finally {
outStream.close()
}
// Fix up binaries
srcBoot.delete()
"cp_readlink $installDir".sh()
return true
}
private fun patchBoot(): Boolean {
val newBoot = installDir.getChildFile("new-boot.img")
if (!useRootDir) {
// Create output files before hand
newBoot.createNewFile()
File(installDir, "stock_boot.img").createNewFile()
}
val cmds = arrayOf(
"cd $installDir",
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
"KEEPVERITY=${Config.keepVerity} " +
"PATCHVBMETAFLAG=${Info.patchBootVbmeta} " +
"RECOVERYMODE=${Config.recovery} " +
"LEGACYSAR=${Info.legacySAR} " +
"sh boot_patch.sh $srcBoot")
val isSuccess = cmds.sh().isSuccess
shell.newJob().add("./magiskboot cleanup", "cd /").exec()
return isSuccess
}
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
private suspend fun postOTA(): Boolean {
try {
val bootctl = File.createTempFile("bootctl", null, context.cacheDir)
context.assets.open("bootctl").writeTo(bootctl)
"post_ota $bootctl".sh()
} catch (e: IOException) {
console.add("! Unable to download bootctl")
Timber.e(e)
return false
}
console.add("*************************************************************")
console.add(" Next reboot will boot to second slot!")
console.add(" Go back to System Updates and press Restart to complete OTA")
console.add("*************************************************************")
return true
}
private fun Array<String>.eq() = shell.newJob().add(*this).to(console, logs).enqueue()
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
protected suspend fun patchFile(file: Uri) = extractFiles() && processFile(file)
protected suspend fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
protected suspend fun secondSlot() =
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
protected suspend fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
protected fun restore() = findImage() && "restore_imgs $srcBoot".sh().isSuccess
protected fun uninstall() = "run_uninstaller $AppApkPath".sh().isSuccess
@WorkerThread
protected abstract suspend fun operations(): Boolean
open suspend fun exec(): Boolean {
if (haveActiveSession.getAndSet(true))
return false
val result = withContext(Dispatchers.IO) { operations() }
haveActiveSession.set(false)
if (result)
return true
// Not every operation initializes installDir
if (::installDir.isInitialized)
Shell.cmd("rm -rf $installDir").submit()
return false
}
companion object {
private var haveActiveSession = AtomicBoolean(false)
}
}
abstract class ConsoleInstaller(
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstallImpl(console, logs) {
override suspend fun exec(): Boolean {
val success = super.exec()
if (success) {
console.add("- All done!")
} else {
console.add("! Installation failed")
}
return success
}
}
abstract class CallBackInstaller : MagiskInstallImpl(DummyList, DummyList) {
suspend fun exec(callback: (Boolean) -> Unit): Boolean {
val success = exec()
callback(success)
return success
}
}
class MagiskInstaller {
class Patch(
private val uri: Uri,
console: MutableList<String>,
logs: MutableList<String>
) : ConsoleInstaller(console, logs) {
override suspend fun operations() = patchFile(uri)
}
class SecondSlot(
console: MutableList<String>,
logs: MutableList<String>
) : ConsoleInstaller(console, logs) {
override suspend fun operations() = secondSlot()
}
class Direct(
console: MutableList<String>,
logs: MutableList<String>
) : ConsoleInstaller(console, logs) {
override suspend fun operations() = direct()
}
class Emulator(
console: MutableList<String>,
logs: MutableList<String>
) : ConsoleInstaller(console, logs) {
override suspend fun operations() = fixEnv()
}
class Uninstall(
console: MutableList<String>,
logs: MutableList<String>
) : ConsoleInstaller(console, logs) {
override suspend fun operations() = uninstall()
override suspend fun exec(): Boolean {
val success = super.exec()
if (success) {
UiThreadHandler.handler.postDelayed(3000) {
Shell.cmd("pm uninstall ${context.packageName}").exec()
}
}
return success
}
}
class Restore : CallBackInstaller() {
override suspend fun operations() = restore()
}
class FixEnv : CallBackInstaller() {
override suspend fun operations() = fixEnv()
}
}

View file

@ -0,0 +1,124 @@
package com.topjohnwu.magisk.core.utils
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder.LITTLE_ENDIAN
import java.nio.charset.Charset
class AXML(b: ByteArray) {
var bytes = b
private set
companion object {
private const val CHUNK_SIZE_OFF = 4
private const val STRING_INDICES_OFF = 7 * 4
private val UTF_16LE = Charset.forName("UTF-16LE")
}
/**
* String pool header:
* 0: 0x1C0001
* 1: chunk size
* 2: number of strings
* 3: number of styles (assert as 0)
* 4: flags
* 5: offset to string data
* 6: offset to style data (assert as 0)
*
* Followed by an array of uint32_t with size = number of strings
* Each entry points to an offset into the string data
*/
fun patchStrings(mapFn: (String) -> String): Boolean {
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
fun findStringPool(): Int {
var offset = 8
while (offset < bytes.size) {
if (buffer.getInt(offset) == 0x1C0001)
return offset
offset += buffer.getInt(offset + CHUNK_SIZE_OFF)
}
return -1
}
val start = findStringPool()
if (start < 0)
return false
// Read header
buffer.position(start + 4)
val intBuf = buffer.asIntBuffer()
val size = intBuf.get()
val count = intBuf.get()
intBuf.get()
intBuf.get()
val dataOff = start + intBuf.get()
intBuf.get()
val strList = ArrayList<String>(count)
// Collect all strings in the pool
for (i in 0 until count) {
val off = dataOff + intBuf.get()
val len = buffer.getShort(off)
strList.add(String(bytes, off + 2, len * 2, UTF_16LE))
}
val strArr = strList.toTypedArray()
for (i in strArr.indices) {
strArr[i] = mapFn(strArr[i])
}
// Write everything before string data, will patch values later
val baos = RawByteStream()
baos.write(bytes, 0, dataOff)
// Write string data
val offList = IntArray(count)
for (i in 0 until count) {
offList[i] = baos.size() - dataOff
val str = strArr[i]
baos.write(str.length.toShortBytes())
baos.write(str.toByteArray(UTF_16LE))
// Null terminate
baos.write(0)
baos.write(0)
}
baos.align()
val sizeDiff = baos.size() - start - size
val newBuffer = ByteBuffer.wrap(baos.buffer).order(LITTLE_ENDIAN)
// Patch XML size
newBuffer.putInt(CHUNK_SIZE_OFF, buffer.getInt(CHUNK_SIZE_OFF) + sizeDiff)
// Patch string pool size
newBuffer.putInt(start + CHUNK_SIZE_OFF, size + sizeDiff)
// Patch index table
newBuffer.position(start + STRING_INDICES_OFF)
val newIntBuf = newBuffer.asIntBuffer()
offList.forEach { newIntBuf.put(it) }
// Write the rest of the chunks
val nextOff = start + size
baos.write(bytes, nextOff, bytes.size - nextOff)
bytes = baos.toByteArray()
return true
}
private fun Int.toShortBytes(): ByteArray {
val b = ByteBuffer.allocate(2).order(LITTLE_ENDIAN)
b.putShort(this.toShort())
return b.array()
}
private class RawByteStream : ByteArrayOutputStream() {
val buffer: ByteArray get() = buf
fun align(alignment: Int = 4) {
val newCount = (count + alignment - 1) / alignment * alignment
for (i in 0 until (newCount - count))
write(0)
}
}
}

View file

@ -0,0 +1,47 @@
package com.topjohnwu.magisk.core.utils;
import android.os.Build;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipUtil;
import java.nio.file.attribute.FileTime;
import java.util.zip.ZipEntry;
public class Desugar {
public static FileTime getLastModifiedTime(ZipEntry entry) {
if (Build.VERSION.SDK_INT >= 26) {
return entry.getLastModifiedTime();
} else {
return FileTime.fromMillis(entry.getTime());
}
}
public static FileTime getLastAccessTime(ZipEntry entry) {
if (Build.VERSION.SDK_INT >= 26) {
return entry.getLastAccessTime();
} else {
return null;
}
}
public static FileTime getCreationTime(ZipEntry entry) {
if (Build.VERSION.SDK_INT >= 26) {
return entry.getCreationTime();
} else {
return null;
}
}
/**
* Within {@link ZipArchiveOutputStream#copyFromZipInputStream}, we redirect the method call
* {@link ZipUtil#checkRequestedFeatures} to this method. This is safe because the only usage
* of copyFromZipInputStream is in {@link ZipArchiveOutputStream#addRawArchiveEntry},
* which does not need to actually understand the content of the zip entry. By removing
* this feature check, we can modify zip files using unsupported compression methods.
*/
public static void checkRequestedFeatures(final ZipArchiveEntry ze) {
// No-op
}
}

View file

@ -0,0 +1,16 @@
package com.topjohnwu.magisk.core.utils
object DummyList : java.util.AbstractList<String>() {
override val size: Int get() = 0
override fun get(index: Int): String {
throw IndexOutOfBoundsException()
}
override fun add(element: String): Boolean = false
override fun add(index: Int, element: String) {}
override fun clear() {}
}

View file

@ -0,0 +1,80 @@
package com.topjohnwu.magisk.core.utils
import android.util.Base64
import android.util.Base64OutputStream
import com.topjohnwu.magisk.core.Config
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import java.io.ByteArrayOutputStream
import java.math.BigInteger
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.util.Calendar
import java.util.Locale
import java.util.Random
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
private interface CertKeyProvider {
val cert: X509Certificate
val key: PrivateKey
}
class Keygen : CertKeyProvider {
companion object {
private const val ALIAS = "magisk"
private val PASSWORD get() = "magisk".toCharArray()
private const val DNAME = "C=US,ST=California,L=Mountain View,O=Google Inc.,OU=Android,CN=Android"
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
}
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
private val end = (start.clone() as Calendar).apply { add(Calendar.YEAR, 30) }
private val ks = init()
override val cert = ks.getCertificate(ALIAS) as X509Certificate
override val key = ks.getKey(ALIAS, PASSWORD) as PrivateKey
private fun init(): KeyStore {
val raw = Config.keyStoreRaw
val ks = KeyStore.getInstance("PKCS12")
if (raw.isEmpty()) {
ks.load(null)
} else {
GZIPInputStream(Base64.decode(raw, BASE64_FLAG).inputStream()).use {
ks.load(it, PASSWORD)
}
}
// Keys already exist
if (ks.containsAlias(ALIAS))
return ks
// Generate new private key and certificate
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
val dname = X500Name(DNAME)
val builder = X509v3CertificateBuilder(
dname, BigInteger(160, Random()),
start.time, end.time, Locale.ROOT, dname,
SubjectPublicKeyInfo.getInstance(kp.public.encoded)
)
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
// Store them into keystore
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert))
val bytes = ByteArrayOutputStream()
GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use {
ks.store(it, PASSWORD)
}
Config.keyStoreRaw = bytes.toString("UTF-8")
return ks
}
}

View file

@ -0,0 +1,183 @@
package com.topjohnwu.magisk.core.utils
import android.annotation.SuppressLint
import android.app.LocaleConfig
import android.app.LocaleManager
import android.content.ContextWrapper
import android.content.res.Resources
import android.os.Build
import android.os.LocaleList
import androidx.annotation.RequiresApi
import com.topjohnwu.magisk.core.AppApkPath
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.base.relaunch
import com.topjohnwu.magisk.core.isRunningAsStub
import org.xmlpull.v1.XmlPullParser
import java.util.Locale
interface LocaleSetting {
// The locale that is manually overridden, null if system default
val appLocale: Locale?
// The current active locale used in the application
val currentLocale: Locale
fun setLocale(tag: String)
fun updateResource(res: Resources)
private class Api23Impl : LocaleSetting {
private val systemLocale: Locale = Locale.getDefault()
override var currentLocale: Locale = systemLocale
override var appLocale: Locale? = null
init {
setLocale(Config.locale)
}
override fun setLocale(tag: String) {
val locale = when {
tag.isEmpty() -> null
else -> Locale.forLanguageTag(tag)
}
currentLocale = locale ?: systemLocale
appLocale = locale
Locale.setDefault(currentLocale)
updateResource(AppContext.resources)
AppContext.foregroundActivity?.relaunch()
}
@Suppress("DEPRECATION")
override fun updateResource(res: Resources) {
val config = res.configuration
config.setLocale(currentLocale)
res.updateConfiguration(config, null)
}
}
@RequiresApi(24)
private class Api24Impl : LocaleSetting {
private val systemLocaleList = LocaleList.getDefault()
private var currentLocaleList: LocaleList = systemLocaleList
override var appLocale: Locale? = null
override val currentLocale: Locale get() = currentLocaleList[0]
init {
setLocale(Config.locale)
}
override fun setLocale(tag: String) {
val localeList = when {
tag.isEmpty() -> null
else -> LocaleList.forLanguageTags(tag)
}
currentLocaleList = localeList ?: systemLocaleList
appLocale = localeList?.get(0)
LocaleList.setDefault(currentLocaleList)
updateResource(AppContext.resources)
AppContext.foregroundActivity?.relaunch()
}
@Suppress("DEPRECATION")
override fun updateResource(res: Resources) {
val config = res.configuration
config.setLocales(currentLocaleList)
res.updateConfiguration(config, null)
}
}
@RequiresApi(33)
private class Api33Impl : LocaleSetting {
private val lm: LocaleManager = AppContext.getSystemService(LocaleManager::class.java)
override val appLocale: Locale?
get() = lm.applicationLocales.let { if (it.isEmpty) null else it[0] }
override val currentLocale: Locale
get() = appLocale ?: lm.systemLocales[0]
// These following methods should not be used
override fun setLocale(tag: String) {}
override fun updateResource(res: Resources) {}
}
class AppLocaleList(
val names: Array<String>,
val tags: Array<String>
)
@SuppressLint("NewApi")
companion object {
val available: AppLocaleList by lazy {
val names = ArrayList<String>()
val tags = ArrayList<String>()
names.add(AppContext.getString(R.string.system_default))
tags.add("")
if (Build.VERSION.SDK_INT >= 34) {
// Use platform LocaleConfig parser
val config = localeConfig
val list = config.supportedLocales ?: LocaleList.getEmptyLocaleList()
names.ensureCapacity(list.size() + 1)
tags.ensureCapacity(list.size() + 1)
for (i in 0 until list.size()) {
val locale = list[i]
names.add(locale.getDisplayName(locale))
tags.add(locale.toLanguageTag())
}
} else {
// Manually parse locale_config.xml
val parser = AppContext.resources.getXml(R.xml.locale_config)
while (true) {
when (parser.next()) {
XmlPullParser.START_TAG -> {
if (parser.name == "locale") {
val tag = parser.getAttributeValue(0)
val locale = Locale.forLanguageTag(tag)
names.add(locale.getDisplayName(locale))
tags.add(tag)
}
}
XmlPullParser.END_DOCUMENT -> break
}
}
}
AppLocaleList(names.toTypedArray(), tags.toTypedArray())
}
@get:RequiresApi(34)
val localeConfig: LocaleConfig by lazy {
val context = if (isRunningAsStub) {
val pkgInfo = AppContext.packageManager.getPackageArchiveInfo(AppApkPath, 0)!!
object : ContextWrapper(AppContext) {
override fun getApplicationInfo() = pkgInfo.applicationInfo
}
} else {
AppContext
}
LocaleConfig.fromContextIgnoringOverride(context)
}
val useLocaleManager get() =
if (isRunningAsStub) Build.VERSION.SDK_INT >= 34
else Build.VERSION.SDK_INT >= 33
val instance: LocaleSetting by lazy {
// Initialize available locale list
available
if (useLocaleManager) {
Api33Impl()
} else if (Build.VERSION.SDK_INT <= 23) {
Api23Impl()
} else {
Api24Impl()
}
}
}
}

View file

@ -0,0 +1,138 @@
package com.topjohnwu.magisk.core.utils
import android.content.ContentUris
import android.content.ContentValues
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.provider.OpenableColumns
import androidx.annotation.RequiresApi
import androidx.core.net.toFile
import androidx.core.net.toUri
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Config
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
@Suppress("DEPRECATION")
object MediaStoreUtils {
private val cr get() = AppContext.contentResolver
private fun relativePath(name: String) =
if (name.isEmpty()) Environment.DIRECTORY_DOWNLOADS
else Environment.DIRECTORY_DOWNLOADS + File.separator + name
fun fullPath(name: String): String =
File(Environment.getExternalStorageDirectory(), relativePath(name)).canonicalPath
private val downloadPath get() = relativePath(Config.downloadDir)
@RequiresApi(api = 30)
@Throws(IOException::class)
private fun insertFile(displayName: String): MediaStoreFile {
val values = ContentValues()
values.put(MediaStore.MediaColumns.RELATIVE_PATH, downloadPath)
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
// When a file with the same name exists and was not created by us:
// - Before Android 11, insert will return null
// - On Android 11+, the system will automatically create a new name
// Thus the reason to restrict this method call to API 30+
val fileUri = cr.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
?: throw IOException("Can't insert $displayName.")
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
cr.query(fileUri, projection, null, null, null)?.use { cursor ->
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
if (cursor.moveToFirst()) {
val id = cursor.getLong(idIndex)
val data = cursor.getString(dataColumn)
return MediaStoreFile(id, data)
}
}
throw IOException("Can't insert $displayName.")
}
@RequiresApi(api = 29)
private fun queryFile(displayName: String): UriFile? {
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
// Before Android 10, we wrote the DISPLAY_NAME field when insert, so it can be used.
val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} == ?"
val selectionArgs = arrayOf(displayName)
val sortOrder = "${MediaStore.MediaColumns.DATE_ADDED} DESC"
val query = cr.query(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
projection, selection, selectionArgs, sortOrder)
query?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val data = cursor.getString(dataColumn)
if (data.endsWith(downloadPath + File.separator + displayName)) {
return MediaStoreFile(id, data)
}
}
}
return null
}
@Throws(IOException::class)
fun getFile(displayName: String): UriFile {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// Fallback to file based I/O pre Android 11
val parent = File(Environment.getExternalStorageDirectory(), downloadPath)
parent.mkdirs()
LegacyUriFile(File(parent, displayName))
} else {
queryFile(displayName) ?: insertFile(displayName)
}
}
fun Uri.inputStream() = cr.openInputStream(this) ?: throw FileNotFoundException()
fun Uri.outputStream() = cr.openOutputStream(this, "rwt") ?: throw FileNotFoundException()
val Uri.displayName: String get() {
if (scheme == "file") {
// Simple uri wrapper over file, directly get file name
return toFile().name
}
require(scheme == "content") { "Uri lacks 'content' scheme: $this" }
val projection = arrayOf(OpenableColumns.DISPLAY_NAME)
cr.query(this, projection, null, null, null)?.use { cursor ->
val displayNameColumn = cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
if (cursor.moveToFirst()) {
return cursor.getString(displayNameColumn)
}
}
return this.toString()
}
interface UriFile {
val uri: Uri
fun delete(): Boolean
}
private class LegacyUriFile(private val file: File) : UriFile {
override val uri = file.toUri()
override fun delete() = file.delete()
override fun toString() = file.toString()
}
@RequiresApi(api = 29)
private class MediaStoreFile(private val id: Long, private val data: String) : UriFile {
override val uri = ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)
override fun toString() = data
override fun delete(): Boolean {
val selection = "${MediaStore.MediaColumns._ID} == ?"
val selectionArgs = arrayOf(id.toString())
return cr.delete(uri, selection, selectionArgs) == 1
}
}
}

View file

@ -0,0 +1,74 @@
package com.topjohnwu.magisk.core.utils
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.PowerManager
import androidx.collection.ArraySet
import androidx.core.content.getSystemService
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.ktx.registerRuntimeReceiver
class NetworkObserver(context: Context) {
private val manager = context.getSystemService<ConnectivityManager>()!!
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
private val activeList = ArraySet<Network>()
override fun onAvailable(network: Network) {
activeList.add(network)
postValue(true)
}
override fun onLost(network: Network) {
activeList.remove(network)
postValue(!activeList.isEmpty())
}
}
private val receiver = object : BroadcastReceiver() {
private fun Context.isIdleMode(): Boolean {
val pwm = getSystemService<PowerManager>() ?: return true
val isIgnoringOptimizations = pwm.isIgnoringBatteryOptimizations(packageName)
return pwm.isDeviceIdleMode && !isIgnoringOptimizations
}
override fun onReceive(context: Context, intent: Intent) {
if (context.isIdleMode()) {
postValue(false)
} else {
postCurrentState()
}
}
}
init {
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
manager.registerNetworkCallback(request, networkCallback)
val filter = IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
context.applicationContext.registerRuntimeReceiver(receiver, filter)
}
fun postCurrentState() {
postValue(
manager.getNetworkCapabilities(manager.activeNetwork)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == true
)
}
private fun postValue(b: Boolean) {
Info.resetUpdate()
Info.isConnected.postValue(b)
}
companion object {
fun init(context: Context): NetworkObserver {
return NetworkObserver(context).apply { postCurrentState() }
}
}
}

View file

@ -0,0 +1,48 @@
package com.topjohnwu.magisk.core.utils
import java.io.FilterInputStream
import java.io.InputStream
class ProgressInputStream(
base: InputStream,
val progressEmitter: (Long) -> Unit
) : FilterInputStream(base) {
private var bytesRead = 0L
private var lastUpdate = 0L
private fun emitProgress() {
val cur = System.currentTimeMillis()
if (cur - lastUpdate > 1000) {
lastUpdate = cur
progressEmitter(bytesRead)
}
}
override fun read(): Int {
val b = read()
if (b >= 0) {
bytesRead++
emitProgress()
}
return b
}
override fun read(b: ByteArray): Int {
return read(b, 0, b.size)
}
override fun read(b: ByteArray, off: Int, len: Int): Int {
val sz = super.read(b, off, len)
if (sz > 0) {
bytesRead += sz
emitProgress()
}
return sz
}
override fun close() {
super.close()
progressEmitter(bytesRead)
}
}

View file

@ -0,0 +1,17 @@
package com.topjohnwu.magisk.core.utils
import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
class RequestAuthentication: ActivityResultContract<Unit, Boolean>() {
override fun createIntent(context: Context, input: Unit) =
context.getSystemService(KeyguardManager::class.java)
.createConfirmDeviceCredentialIntent(null, null)
override fun parseResult(resultCode: Int, intent: Intent?) =
resultCode == Activity.RESULT_OK
}

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 < Build.VERSION_CODES.O)
return SynchronousResult(true)
if (context.packageManager.canRequestPackageInstalls())
return SynchronousResult(true)
return null
}
}

View file

@ -0,0 +1,169 @@
package com.topjohnwu.magisk.core.utils
import android.app.ActivityManager
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.system.Os
import androidx.core.content.getSystemService
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.ipc.RootService
import com.topjohnwu.superuser.nio.FileSystemManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.util.concurrent.locks.AbstractQueuedSynchronizer
class RootUtils(stub: Any?) : RootService() {
private val className: String = stub?.javaClass?.name ?: javaClass.name
private lateinit var am: ActivityManager
constructor() : this(null)
init {
Timber.plant(object : Timber.DebugTree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
super.log(priority, "Magisk", message, t)
}
})
}
override fun onCreate() {
am = getSystemService()!!
}
override fun getComponentName(): ComponentName {
return ComponentName(packageName, className)
}
override fun onBind(intent: Intent): IBinder {
return object : IRootUtils.Stub() {
override fun getAppProcess(pid: Int) = safe(null) { getAppProcessImpl(pid) }
override fun getFileSystem(): IBinder = FileSystemManager.getService()
override fun addSystemlessHosts() = safe(false) { addSystemlessHostsImpl() }
}
}
private fun getAppProcessImpl(_pid: Int): ActivityManager.RunningAppProcessInfo? {
val procList = am.runningAppProcesses
var pid = _pid
while (pid > 1) {
val proc = procList.find { it.pid == pid }
if (proc != null)
return proc
// Stop find when root process
if (Os.stat("/proc/$pid").st_uid == 0) {
return null
}
// Find PPID
File("/proc/$pid/status").useLines {
val line = it.find { l -> l.startsWith("PPid:") } ?: return null
pid = line.substring(5).trim().toInt()
}
}
return null
}
private fun addSystemlessHostsImpl(): Boolean {
val module = File(Const.MODULE_PATH, "hosts")
if (module.exists()) return true
val hosts = File(module, "system/etc/hosts")
if (!hosts.parentFile.mkdirs()) return false
File(module, "module.prop").outputStream().writer().use {
it.write("""
id=hosts
name=Systemless Hosts
version=1.0
versionCode=1
author=Magisk
description=Magisk app built-in systemless hosts module
""".trimIndent())
}
File("/system/etc/hosts").copyTo(hosts)
File(module, "update").createNewFile()
return true
}
object Connection : AbstractQueuedSynchronizer(), ServiceConnection {
init {
state = 1
}
override fun onServiceConnected(name: ComponentName, service: IBinder) {
Timber.d("onServiceConnected")
IRootUtils.Stub.asInterface(service).let {
obj = it
fs = FileSystemManager.getRemote(it.fileSystem)
}
releaseShared(1)
}
override fun onServiceDisconnected(name: ComponentName) {
state = 1
obj = null
bind(Intent().setComponent(name), this)
}
override fun tryAcquireShared(acquires: Int) = if (state == 0) 1 else -1
override fun tryReleaseShared(releases: Int): Boolean {
// Decrement count; signal when transition to zero
while (true) {
val c = state
if (c == 0)
return false
val n = c - 1
if (compareAndSetState(c, n))
return n == 0
}
}
fun await() {
if (!Info.isRooted)
return
if (!ShellUtils.onMainThread()) {
acquireSharedInterruptibly(1)
} else if (state != 0) {
throw IllegalStateException("Cannot await on the main thread")
}
}
}
companion object {
var bindTask: Shell.Task? = null
var fs: FileSystemManager = FileSystemManager.getLocal()
get() {
Connection.await()
return field
}
private set
private var obj: IRootUtils? = null
get() {
Connection.await()
return field
}
fun getAppProcess(pid: Int) = safe(null) { obj?.getAppProcess(pid) }
suspend fun addSystemlessHosts() =
withContext(Dispatchers.IO) { safe(false) { obj?.addSystemlessHosts() ?: false } }
private inline fun <T> safe(default: T, block: () -> T): T {
return try {
block()
} catch (e: Throwable) {
// The process died unexpectedly
Timber.e(e)
default
}
}
}
}

View file

@ -0,0 +1,75 @@
package com.topjohnwu.magisk.core.utils
import android.content.Context
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.cachedFile
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import java.io.File
import java.util.jar.JarFile
class ShellInit : Shell.Initializer() {
override fun onInit(context: Context, shell: Shell): Boolean {
if (shell.isRoot) {
Info.isRooted = true
RootUtils.bindTask?.let { shell.execTask(it) }
RootUtils.bindTask = null
}
shell.newJob().apply {
add("export ASH_STANDALONE=1")
val localBB: File
if (isRunningAsStub) {
if (!shell.isRoot)
return true
val jar = JarFile(StubApk.current(context))
val bb = jar.getJarEntry("lib/${Const.CPU_ABI}/libbusybox.so")
localBB = context.deviceProtectedContext.cachedFile("busybox")
localBB.delete()
runBlocking {
jar.getInputStream(bb).writeTo(localBB, dispatcher = Dispatchers.Unconfined)
}
localBB.setExecutable(true)
} else {
localBB = File(context.applicationInfo.nativeLibraryDir, "libbusybox.so")
}
if (shell.isRoot) {
add("export MAGISKTMP=\$(magisk --path)")
// Test if we can properly execute stuff in /data
Info.noDataExec = !shell.newJob()
.add("$localBB sh -c '$localBB true'").exec().isSuccess
}
if (Info.noDataExec) {
// Copy it out of /data to workaround Samsung bullshit
add(
"if [ -x \$MAGISKTMP/.magisk/busybox/busybox ]; then",
" cp -af $localBB \$MAGISKTMP/.magisk/busybox/busybox",
" exec \$MAGISKTMP/.magisk/busybox/busybox sh",
"else",
" cp -af $localBB /dev/busybox",
" exec /dev/busybox sh",
"fi"
)
} else {
// Directly execute the file
add("exec $localBB sh")
}
add(context.assets.open("app_functions.sh"))
if (shell.isRoot) {
add(context.assets.open("util_functions.sh"))
}
}.exec()
Info.init(shell)
return true
}
}

View file

@ -0,0 +1,105 @@
package com.topjohnwu.magisk.view
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toIcon
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.ktx.getBitmap
import com.topjohnwu.magisk.core.ktx.selfLaunchIntent
import java.util.concurrent.atomic.AtomicInteger
@Suppress("DEPRECATION")
object Notifications {
val mgr by lazy { AppContext.getSystemService<NotificationManager>()!! }
private const val APP_UPDATED_ID = 4
private const val APP_UPDATE_AVAILABLE_ID = 5
private const val UPDATE_CHANNEL = "update"
private const val PROGRESS_CHANNEL = "progress"
private const val UPDATED_CHANNEL = "updated"
private val nextId = AtomicInteger(APP_UPDATE_AVAILABLE_ID)
fun setup() {
AppContext.apply {
if (SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(UPDATE_CHANNEL,
getString(R.string.update_channel), NotificationManager.IMPORTANCE_DEFAULT)
val channel2 = NotificationChannel(PROGRESS_CHANNEL,
getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW)
val channel3 = NotificationChannel(UPDATED_CHANNEL,
getString(R.string.updated_channel), NotificationManager.IMPORTANCE_HIGH)
mgr.createNotificationChannels(listOf(channel, channel2, channel3))
}
}
}
@SuppressLint("InlinedApi")
fun updateDone() {
AppContext.apply {
val flag = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
val pending = PendingIntent.getActivity(this, 0, selfLaunchIntent(), flag)
val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, UPDATED_CHANNEL)
.setSmallIcon(getBitmap(R.drawable.ic_magisk_outline).toIcon())
} else {
Notification.Builder(this).setPriority(Notification.PRIORITY_HIGH)
.setSmallIcon(R.drawable.ic_magisk_outline)
}
.setContentIntent(pending)
.setContentTitle(getText(R.string.updated_title))
.setContentText(getText(R.string.updated_text))
.setAutoCancel(true)
mgr.notify(APP_UPDATED_ID, builder.build())
}
}
fun updateAvailable() {
AppContext.apply {
val intent = DownloadEngine.getPendingIntent(this, Subject.App())
val bitmap = getBitmap(R.drawable.ic_magisk_outline)
val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, UPDATE_CHANNEL)
.setSmallIcon(bitmap.toIcon())
} else {
Notification.Builder(this)
.setSmallIcon(R.drawable.ic_magisk_outline)
}
.setLargeIcon(bitmap)
.setContentTitle(getString(R.string.magisk_update_title))
.setContentText(getString(R.string.manager_download_install))
.setAutoCancel(true)
.setContentIntent(intent)
mgr.notify(APP_UPDATE_AVAILABLE_ID, builder.build())
}
}
fun startProgress(title: CharSequence): Notification.Builder {
val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(AppContext, PROGRESS_CHANNEL)
} else {
Notification.Builder(AppContext).setPriority(Notification.PRIORITY_LOW)
}
.setSmallIcon(android.R.drawable.stat_sys_download)
.setContentTitle(title)
.setProgress(0, 0, true)
.setOngoing(true)
if (SDK_INT >= Build.VERSION_CODES.S)
builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
return builder
}
fun nextId() = nextId.incrementAndGet()
}

View file

@ -0,0 +1,95 @@
package com.topjohnwu.magisk.view
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.getBitmap
object Shortcuts {
fun setupDynamic(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
val manager = context.getSystemService<ShortcutManager>() ?: return
manager.dynamicShortcuts = getShortCuts(context)
}
}
fun addHomeIcon(context: Context) {
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) ?: return
val info = ShortcutInfoCompat.Builder(context, Const.Nav.HOME)
.setShortLabel(context.getString(R.string.magisk))
.setIntent(intent)
.setIcon(context.getIconCompat(R.drawable.ic_launcher))
.build()
ShortcutManagerCompat.requestPinShortcut(context, info, null)
}
private fun Context.getIcon(id: Int): Icon {
return if (isRunningAsStub) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
Icon.createWithAdaptiveBitmap(getBitmap(id))
else
Icon.createWithBitmap(getBitmap(id))
} else {
Icon.createWithResource(this, id)
}
}
private fun Context.getIconCompat(id: Int): IconCompat {
return if (isRunningAsStub) {
val bitmap = getBitmap(id)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
IconCompat.createWithAdaptiveBitmap(bitmap)
else
IconCompat.createWithBitmap(bitmap)
} else {
IconCompat.createWithResource(this, id)
}
}
@RequiresApi(api = 25)
private fun getShortCuts(context: Context): List<ShortcutInfo> {
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
?: return emptyList()
val shortCuts = mutableListOf<ShortcutInfo>()
if (Info.showSuperUser) {
shortCuts.add(
ShortcutInfo.Builder(context, Const.Nav.SUPERUSER)
.setShortLabel(context.getString(R.string.superuser))
.setIntent(
Intent(intent).putExtra(Const.Key.OPEN_SECTION, Const.Nav.SUPERUSER)
)
.setIcon(context.getIcon(R.drawable.sc_superuser))
.setRank(0)
.build()
)
}
if (Info.env.isActive) {
shortCuts.add(
ShortcutInfo.Builder(context, Const.Nav.MODULES)
.setShortLabel(context.getString(R.string.modules))
.setIntent(
Intent(intent).putExtra(Const.Key.OPEN_SECTION, Const.Nav.MODULES)
)
.setIcon(context.getIcon(R.drawable.sc_extension))
.setRank(1)
.build()
)
}
return shortCuts
}
}

View file

@ -0,0 +1,161 @@
package com.topjohnwu.magisk.test
import android.os.ParcelFileDescriptor.AutoCloseInputStream
import androidx.annotation.Keep
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.test.Environment.Companion.EMPTY_ZYGISK
import com.topjohnwu.magisk.test.Environment.Companion.INVALID_ZYGISK
import com.topjohnwu.magisk.test.Environment.Companion.MOUNT_TEST
import com.topjohnwu.magisk.test.Environment.Companion.REMOVE_TEST
import com.topjohnwu.magisk.test.Environment.Companion.SEPOLICY_RULE
import com.topjohnwu.magisk.test.Environment.Companion.UPGRADE_TEST
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
@Keep
@RunWith(AndroidJUnit4::class)
class AdditionalTest : BaseTest {
companion object {
private const val SHELL_PKG = "com.android.shell"
private const val LSPOSED_CATEGORY = "org.lsposed.manager.LAUNCH_MANAGER"
private const val LSPOSED_PKG = "org.lsposed.manager"
private lateinit var modules: List<LocalModule>
@BeforeClass
@JvmStatic
fun before() {
BaseTest.prerequisite()
runBlocking {
modules = LocalModule.installed()
}
}
}
@After
fun teardown() {
device.pressHome()
}
@Test
fun testModuleCount() {
var expected = 4
if (Environment.mount()) expected++
if (Environment.preinit()) expected++
if (Environment.lsposed()) expected++
if (Environment.shamiko()) expected++
assertEquals("Module count incorrect", expected, modules.size)
}
@Test
fun testLsposed() {
assumeTrue(Environment.lsposed())
val module = modules.find { it.id == "zygisk_lsposed" }
assertNotNull("zygisk_lsposed is not installed", module)
module!!
assertFalse("zygisk_lsposed is not enabled", module.zygiskUnloaded)
// Launch lsposed manager to ensure the module is active
uiAutomation.executeShellCommand(
"am start -c $LSPOSED_CATEGORY $SHELL_PKG/.BugreportWarningActivity"
).let { pfd -> AutoCloseInputStream(pfd).use { it.readBytes() } }
val pattern = Pattern.compile("$LSPOSED_PKG:id/.*")
assertNotNull(
"LSPosed manager launch failed",
device.wait(Until.hasObject(By.res(pattern)), TimeUnit.SECONDS.toMillis(10))
)
}
@Test
fun testModuleMount() {
assumeTrue(Environment.mount())
assertNotNull("$MOUNT_TEST is not installed", modules.find { it.id == MOUNT_TEST })
assertTrue(
"/system/fonts/newfile should exist",
RootUtils.fs.getFile("/system/fonts/newfile").exists()
)
assertFalse(
"/system/bin/screenrecord should not exist",
RootUtils.fs.getFile("/system/bin/screenrecord").exists()
)
val egg = RootUtils.fs.getFile("/system/app/EasterEgg").list() ?: arrayOf()
assertArrayEquals(
"/system/app/EasterEgg should be replaced",
egg,
arrayOf("newfile")
)
}
@Test
fun testSepolicyRule() {
assumeTrue(Environment.preinit())
assertNotNull("$SEPOLICY_RULE is not installed", modules.find { it.id == SEPOLICY_RULE })
assertTrue(
"Module sepolicy.rule is not applied",
Shell.cmd("magiskpolicy --print-rules | grep -q magisk_test").exec().isSuccess
)
}
@Test
fun testEmptyZygiskModule() {
val module = modules.find { it.id == EMPTY_ZYGISK }
assertNotNull("$EMPTY_ZYGISK is not installed", module)
module!!
assertTrue("$EMPTY_ZYGISK should be zygisk unloaded", module.zygiskUnloaded)
}
@Test
fun testInvalidZygiskModule() {
val module = modules.find { it.id == INVALID_ZYGISK }
assertNotNull("$INVALID_ZYGISK is not installed", module)
module!!
assertTrue("$INVALID_ZYGISK should be zygisk unloaded", module.zygiskUnloaded)
}
@Test
fun testRemoveModule() {
assertNull("$REMOVE_TEST should be removed", modules.find { it.id == REMOVE_TEST })
assertTrue(
"Uninstaller of $REMOVE_TEST should be run",
RootUtils.fs.getFile(Environment.REMOVE_TEST_MARKER).exists()
)
}
@Test
fun testModuleUpgrade() {
val module = modules.find { it.id == UPGRADE_TEST }
assertNotNull("$UPGRADE_TEST is not installed", module)
module!!
assertFalse("$UPGRADE_TEST should be disabled", module.enable)
assertTrue(
"$UPGRADE_TEST should be updated",
module.base.getChildFile("post-fs-data.sh").exists()
)
assertFalse(
"$UPGRADE_TEST should be updated",
module.base.getChildFile("service.sh").exists()
)
}
}

View file

@ -0,0 +1,27 @@
package com.topjohnwu.magisk.test
import android.app.Instrumentation
import android.app.UiAutomation
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.superuser.Shell
import org.junit.Assert.assertTrue
interface BaseTest {
val instrumentation: Instrumentation
get() = InstrumentationRegistry.getInstrumentation()
val appContext: Context get() = instrumentation.targetContext
val testContext: Context get() = instrumentation.context
val uiAutomation: UiAutomation get() = instrumentation.uiAutomation
val device: UiDevice get() = UiDevice.getInstance(instrumentation)
companion object {
fun prerequisite() {
assertTrue("Should have root access", Shell.getShell().isRoot)
// Make sure the root service is running
RootUtils.Connection.await()
}
}
}

View file

@ -0,0 +1,288 @@
package com.topjohnwu.magisk.test
import android.app.Notification
import android.os.Build
import androidx.annotation.Keep
import androidx.core.net.toUri
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.topjohnwu.magisk.core.BuildConfig.APP_PACKAGE_NAME
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.download.DownloadNotifier
import com.topjohnwu.magisk.core.download.DownloadProcessor
import com.topjohnwu.magisk.core.ktx.cachedFile
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.tasks.FlashZip
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.nio.ExtendedFile
import kotlinx.coroutines.runBlocking
import org.apache.commons.compress.archivers.zip.ZipFile
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import timber.log.Timber
import java.io.File
import java.io.PrintStream
@Keep
@RunWith(AndroidJUnit4::class)
class Environment : BaseTest {
companion object {
@BeforeClass
@JvmStatic
fun before() = BaseTest.prerequisite()
// The kernel running on emulators < API 26 does not play well with
// magic mount. Skip mount_test on those legacy platforms.
fun mount(): Boolean {
return Build.VERSION.SDK_INT >= 26
}
// It is possible that there are no suitable preinit partition to use
fun preinit(): Boolean {
return Shell.cmd("magisk --preinit-device").exec().isSuccess
}
fun lsposed(): Boolean {
return Build.VERSION.SDK_INT in 27..34
}
fun shamiko(): Boolean {
return Build.VERSION.SDK_INT >= 27
}
private const val MODULE_UPDATE_PATH = "/data/adb/modules_update"
private const val MODULE_ERROR = "Module zip processing incorrect"
const val MOUNT_TEST = "mount_test"
const val SEPOLICY_RULE = "sepolicy_rule"
const val INVALID_ZYGISK = "invalid_zygisk"
const val REMOVE_TEST = "remove_test"
const val REMOVE_TEST_MARKER = "/dev/.remove_test_removed"
const val EMPTY_ZYGISK = "empty_zygisk"
const val UPGRADE_TEST = "upgrade_test"
}
object TimberLog : CallbackList<String>(Runnable::run) {
override fun onAddElement(e: String) {
Timber.i(e)
}
}
private fun checkModuleZip(file: File) {
// Make sure module processing is correct
ZipFile.Builder().setFile(file).get().use { zip ->
val meta = zip.entries
.asSequence()
.filter { it.name.startsWith("META-INF") }
.toMutableList()
assertEquals(MODULE_ERROR, 6, meta.size)
val binary = zip.getInputStream(
zip.getEntry("META-INF/com/google/android/update-binary")
).use { it.readBytes() }
val ref = appContext.assets.open("module_installer.sh").use { it.readBytes() }
assertArrayEquals(MODULE_ERROR, ref, binary)
val script = zip.getInputStream(
zip.getEntry("META-INF/com/google/android/updater-script")
).use { it.readBytes() }
assertArrayEquals(MODULE_ERROR, "#MAGISK\n".toByteArray(), script)
}
}
private fun setupMountTest(root: ExtendedFile) {
val error = "$MOUNT_TEST setup failed"
val path = root.getChildFile(MOUNT_TEST)
// Create /system/fonts/newfile
val etc = path.getChildFile("system").getChildFile("fonts")
assertTrue(error, etc.mkdirs())
assertTrue(error, etc.getChildFile("newfile").createNewFile())
// Create /system/app/EasterEgg/.replace
val egg = path.getChildFile("system").getChildFile("app").getChildFile("EasterEgg")
assertTrue(error, egg.mkdirs())
assertTrue(error, egg.getChildFile(".replace").createNewFile())
// Create /system/app/EasterEgg/newfile
assertTrue(error, egg.getChildFile("newfile").createNewFile())
// Delete /system/bin/screenrecord
val bin = path.getChildFile("system").getChildFile("bin")
assertTrue(error, bin.mkdirs())
assertTrue(error, Shell.cmd("mknod $bin/screenrecord c 0 0").exec().isSuccess)
assertTrue(error, Shell.cmd("set_default_perm $path").exec().isSuccess)
}
private fun setupSystemlessHost() {
val error = "hosts setup failed"
assertTrue(error, runBlocking { RootUtils.addSystemlessHosts() })
assertTrue(error, RootUtils.fs.getFile(Const.MODULE_PATH).getChildFile("hosts").exists())
}
private fun setupSepolicyRuleModule(root: ExtendedFile) {
val error = "$SEPOLICY_RULE setup failed"
val path = root.getChildFile(SEPOLICY_RULE)
assertTrue(error, path.mkdirs())
// Add sepolicy patch
PrintStream(path.getChildFile("sepolicy.rule").newOutputStream()).use {
it.println("type magisk_test domain")
}
assertTrue(error, Shell.cmd(
"set_default_perm $path",
"copy_preinit_files"
).exec().isSuccess)
}
private fun setupEmptyZygiskModule(root: ExtendedFile) {
val error = "$EMPTY_ZYGISK setup failed"
val path = root.getChildFile(EMPTY_ZYGISK)
// Create an empty zygisk folder
val module = LocalModule(path)
assertTrue(error, module.zygiskFolder.mkdirs())
}
private fun setupInvalidZygiskModule(root: ExtendedFile) {
val error = "$INVALID_ZYGISK setup failed"
val path = root.getChildFile(INVALID_ZYGISK)
// Create invalid zygisk libraries
val module = LocalModule(path)
assertTrue(error, module.zygiskFolder.mkdirs())
assertTrue(error, module.zygiskFolder.getChildFile("armeabi-v7a.so").createNewFile())
assertTrue(error, module.zygiskFolder.getChildFile("arm64-v8a.so").createNewFile())
assertTrue(error, module.zygiskFolder.getChildFile("x86.so").createNewFile())
assertTrue(error, module.zygiskFolder.getChildFile("x86_64.so").createNewFile())
assertTrue(error, Shell.cmd("set_default_perm $path").exec().isSuccess)
}
private fun setupRemoveModule(root: ExtendedFile) {
val error = "$REMOVE_TEST setup failed"
val path = root.getChildFile(REMOVE_TEST)
// Create a new module but mark is as "remove"
val module = LocalModule(path)
assertTrue(error, path.mkdirs())
// Create uninstaller script
path.getChildFile("uninstall.sh").newOutputStream().writer().use {
it.write("touch $REMOVE_TEST_MARKER")
}
assertTrue(error, path.getChildFile("service.sh").createNewFile())
module.remove = true
assertTrue(error, Shell.cmd("set_default_perm $path").exec().isSuccess)
}
private fun setupUpgradeModule(root: ExtendedFile, update: ExtendedFile) {
val error = "$UPGRADE_TEST setup failed"
val oldPath = root.getChildFile(UPGRADE_TEST)
val newPath = update.getChildFile(UPGRADE_TEST)
// Create an existing module but mark as "disable
val module = LocalModule(oldPath)
assertTrue(error, oldPath.mkdirs())
module.enable = false
// Install service.sh into the old module
assertTrue(error, oldPath.getChildFile("service.sh").createNewFile())
// Create an upgrade module
assertTrue(error, newPath.mkdirs())
// Install post-fs-data.sh into the new module
assertTrue(error, newPath.getChildFile("post-fs-data.sh").createNewFile())
assertTrue(error, Shell.cmd(
"set_default_perm $oldPath",
"set_default_perm $newPath",
).exec().isSuccess)
}
@Test
fun setupEnvironment() {
runBlocking {
assertTrue(
"Magisk setup failed",
MagiskInstaller.Emulator(TimberLog, TimberLog).exec()
)
}
val notify = object : DownloadNotifier {
override val context = appContext
override fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit) {}
}
val processor = DownloadProcessor(notify)
val shamiko = appContext.cachedFile("shamiko.zip")
runBlocking {
testContext.assets.open("shamiko.zip").use {
processor.handleModule(it, shamiko.toUri())
}
checkModuleZip(shamiko)
if (shamiko()) {
assertTrue(
"Shamiko installation failed",
FlashZip(shamiko.toUri(), TimberLog, TimberLog).exec()
)
}
}
val lsp = appContext.cachedFile("lsposed.zip")
runBlocking {
testContext.assets.open("lsposed.zip").use {
processor.handleModule(it, lsp.toUri())
}
checkModuleZip(lsp)
if (lsposed()) {
assertTrue(
"LSPosed installation failed",
FlashZip(lsp.toUri(), TimberLog, TimberLog).exec()
)
}
}
val root = RootUtils.fs.getFile(Const.MODULE_PATH)
val update = RootUtils.fs.getFile(MODULE_UPDATE_PATH)
if (mount()) { setupMountTest(update) }
if (preinit()) { setupSepolicyRuleModule(update) }
setupSystemlessHost()
setupEmptyZygiskModule(update)
setupInvalidZygiskModule(update)
setupRemoveModule(root)
setupUpgradeModule(root, update)
}
@Test
fun setupAppHide() {
runBlocking {
assertTrue(
"App hiding failed",
AppMigration.patchAndHide(
context = appContext,
label = "Settings",
pkg = "repackaged.$APP_PACKAGE_NAME"
)
)
}
}
@Test
fun setupAppRestore() {
runBlocking {
assertTrue(
"App restoration failed",
AppMigration.restoreApp(appContext)
)
}
}
}

View file

@ -0,0 +1,86 @@
package com.topjohnwu.magisk.test
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.ParcelFileDescriptor.AutoCloseInputStream
import androidx.annotation.Keep
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.model.su.SuPolicy
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit
@Keep
@RunWith(AndroidJUnit4::class)
class MagiskAppTest : BaseTest {
companion object {
@BeforeClass
@JvmStatic
fun before() = BaseTest.prerequisite()
}
@Test
fun testZygisk() {
assertTrue("Zygisk should be enabled", Info.isZygiskEnabled)
}
@Test
fun testSuRequest() {
// Bypass the need to actually show a dialog
Config.suAutoResponse = Config.Value.SU_AUTO_ALLOW
Config.prefs.edit().commit()
// Inject an undetermined + mute logging policy for ADB shell
val policy = SuPolicy(
uid = 2000,
logging = false,
notification = false,
remain = 0L
)
runBlocking {
ServiceLocator.policyDB.update(policy)
}
val filter = IntentFilter(Intent.ACTION_VIEW)
filter.addCategory(Intent.CATEGORY_DEFAULT)
val monitor = instrumentation.addMonitor(filter, null, false)
// Try to call su from ADB shell
val cmd = if (Build.VERSION.SDK_INT < 24) {
// API 23 runs executeShellCommand as root
"/system/xbin/su 2000 su -c id"
} else {
"su -c id"
}
val pfd = uiAutomation.executeShellCommand(cmd)
// Make sure SuRequestActivity is launched
val suRequest = monitor.waitForActivityWithTimeout(TimeUnit.SECONDS.toMillis(10))
assertNotNull("SuRequestActivity is not launched", suRequest)
// Check that the request went through
AutoCloseInputStream(pfd).reader().use {
assertTrue(
"Cannot grant root permission from shell",
it.readText().contains("uid=0")
)
}
// Check that the database is updated
runBlocking {
val policy = ServiceLocator.policyDB.fetch(2000)
?: throw AssertionError("PolicyDB is invalid")
assertEquals("Policy for shell is incorrect", SuPolicy.ALLOW, policy.policy)
}
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_magisk_padded" />
<monochrome android:drawable="@drawable/ic_magisk_padded" />
</adaptive-icon>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/light" />
<foreground>
<inset
android:drawable="@drawable/ic_extension"
android:inset="30%" />
</foreground>
<monochrome>
<inset
android:drawable="@drawable/ic_extension"
android:inset="30%" />
</monochrome>
</adaptive-icon>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/light" />
<foreground>
<inset
android:drawable="@drawable/ic_superuser"
android:inset="30%" />
</foreground>
<monochrome>
<inset
android:drawable="@drawable/ic_superuser"
android:inset="30%" />
</monochrome>
</adaptive-icon>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="@color/dark"
android:pathData="M20.5,11H19V7c0,-1.1 -0.9,-2 -2,-2h-4V3.5C13,2.12 11.88,1 10.5,1S8,2.12 8,3.5V5H4c-1.1,0 -1.99,0.9 -1.99,2v3.8H3.5c1.49,0 2.7,1.21 2.7,2.7s-1.21,2.7 -2.7,2.7H2V20c0,1.1 0.9,2 2,2h3.8v-1.5c0,-1.49 1.21,-2.7 2.7,-2.7 1.49,0 2.7,1.21 2.7,2.7V22H17c1.1,0 2,-0.9 2,-2v-4h1.5c1.38,0 2.5,-1.12 2.5,-2.5S21.88,11 20.5,11z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M17.81,4.47c-0.08,0 -0.16,-0.02 -0.23,-0.06C15.66,3.42 14,3 12.01,3c-1.98,0 -3.86,0.47 -5.57,1.41 -0.24,0.13 -0.54,0.04 -0.68,-0.2 -0.13,-0.24 -0.04,-0.55 0.2,-0.68C7.82,2.52 9.86,2 12.01,2c2.13,0 3.99,0.47 6.03,1.52 0.25,0.13 0.34,0.43 0.21,0.67 -0.09,0.18 -0.26,0.28 -0.44,0.28zM3.5,9.72c-0.1,0 -0.2,-0.03 -0.29,-0.09 -0.23,-0.16 -0.28,-0.47 -0.12,-0.7 0.99,-1.4 2.25,-2.5 3.75,-3.27C9.98,4.04 14,4.03 17.15,5.65c1.5,0.77 2.76,1.86 3.75,3.25 0.16,0.22 0.11,0.54 -0.12,0.7 -0.23,0.16 -0.54,0.11 -0.7,-0.12 -0.9,-1.26 -2.04,-2.25 -3.39,-2.94 -2.87,-1.47 -6.54,-1.47 -9.4,0.01 -1.36,0.7 -2.5,1.7 -3.4,2.96 -0.08,0.14 -0.23,0.21 -0.39,0.21zM9.75,21.79c-0.13,0 -0.26,-0.05 -0.35,-0.15 -0.87,-0.87 -1.34,-1.43 -2.01,-2.64 -0.69,-1.23 -1.05,-2.73 -1.05,-4.34 0,-2.97 2.54,-5.39 5.66,-5.39s5.66,2.42 5.66,5.39c0,0.28 -0.22,0.5 -0.5,0.5s-0.5,-0.22 -0.5,-0.5c0,-2.42 -2.09,-4.39 -4.66,-4.39 -2.57,0 -4.66,1.97 -4.66,4.39 0,1.44 0.32,2.77 0.93,3.85 0.64,1.15 1.08,1.64 1.85,2.42 0.19,0.2 0.19,0.51 0,0.71 -0.11,0.1 -0.24,0.15 -0.37,0.15zM16.92,19.94c-1.19,0 -2.24,-0.3 -3.1,-0.89 -1.49,-1.01 -2.38,-2.65 -2.38,-4.39 0,-0.28 0.22,-0.5 0.5,-0.5s0.5,0.22 0.5,0.5c0,1.41 0.72,2.74 1.94,3.56 0.71,0.48 1.54,0.71 2.54,0.71 0.24,0 0.64,-0.03 1.04,-0.1 0.27,-0.05 0.53,0.13 0.58,0.41 0.05,0.27 -0.13,0.53 -0.41,0.58 -0.57,0.11 -1.07,0.12 -1.21,0.12zM14.91,22c-0.04,0 -0.09,-0.01 -0.13,-0.02 -1.59,-0.44 -2.63,-1.03 -3.72,-2.1 -1.4,-1.39 -2.17,-3.24 -2.17,-5.22 0,-1.62 1.38,-2.94 3.08,-2.94 1.7,0 3.08,1.32 3.08,2.94 0,1.07 0.93,1.94 2.08,1.94s2.08,-0.87 2.08,-1.94c0,-3.77 -3.25,-6.83 -7.25,-6.83 -2.84,0 -5.44,1.58 -6.61,4.03 -0.39,0.81 -0.59,1.76 -0.59,2.8 0,0.78 0.07,2.01 0.67,3.61 0.1,0.26 -0.03,0.55 -0.29,0.64 -0.26,0.1 -0.55,-0.04 -0.64,-0.29 -0.49,-1.31 -0.73,-2.61 -0.73,-3.96 0,-1.2 0.23,-2.29 0.68,-3.24 1.33,-2.79 4.28,-4.6 7.51,-4.6 4.55,0 8.25,3.51 8.25,7.83 0,1.62 -1.38,2.94 -3.08,2.94s-3.08,-1.32 -3.08,-2.94c0,-1.07 -0.93,-1.94 -2.08,-1.94s-2.08,0.87 -2.08,1.94c0,1.71 0.66,3.31 1.87,4.51 0.95,0.94 1.86,1.46 3.27,1.85 0.27,0.07 0.42,0.35 0.35,0.61 -0.05,0.23 -0.26,0.38 -0.47,0.38z"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dip"
android:height="24.0dip"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#757575"
android:pathData="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z"/>
</vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="#00AF9C"/>
</shape>
</item>
<item android:drawable="@drawable/ic_magisk" />
</layer-list>

View file

@ -0,0 +1,18 @@
<vector android:height="48dp" android:viewportHeight="720"
android:viewportWidth="720" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#303030" android:pathData="M332.48,421.18c0,0 3.77,22.45 -0.82,71.95c-5.76,62.06 23.64,160.64 23.64,160.64c0,0 40.1,-98.78 33.1,-162.59c-5.75,-52.45 2.6,-70.79 0.82,-68.33c-30.81,42.57 -56.75,-1.67 -56.75,-1.67z"/>
<path android:fillColor="#ffffff" android:pathData="M407.6,474.45c5.01,38.77 -0.57,60.01 -7.81,101.51c-3.66,20.99 74.78,-63.1 104.86,-113.23c5.02,-8.36 -28.77,32.6 -62.19,3.35c-23.18,-20.28 -27.16,-26.44 -45.18,-44.06c-6.08,-5.94 6.74,24.72 10.32,52.43z"/>
<path android:fillColor="#ffffff" android:pathData="M321.99,425.09c-18.02,17.62 -22,23.78 -45.18,44.06c-33.42,29.25 -67.21,-11.71 -62.19,-3.35c30.08,50.13 108.52,134.22 104.86,113.23c-7.24,-41.5 -12.82,-62.74 -7.81,-101.51c3.58,-27.71 16.4,-58.37 10.32,-52.43z"/>
<path android:fillColor="#303030" android:pathData="M399.15,355.87c36.67,10.57 50.89,61.5 87.91,67.8c7.65,1.3 16.27,3.6 26.31,3.12c18.77,-0.9 42.51,-11.51 74.22,-56.5c9.38,-13.3 -23.27,85.66 -105.13,86.86c-59.96,0.88 -66.97,-58.7 -106.93,-60.51c-14.43,-0.65 -15.34,-28.17 -15.34,-28.17c0,0 17.22,-18.86 38.96,-12.6z"/>
<path android:fillColor="#303030" android:pathData="M321.51,355.59c-36.67,10.57 -50.89,61.5 -87.91,67.8c-7.65,1.3 -16.27,3.6 -26.31,3.12c-18.77,-0.9 -42.51,-11.51 -74.22,-56.5c-9.38,-13.3 23.27,85.66 105.13,86.86c59.96,0.88 66.97,-58.7 106.93,-60.51c14.43,-0.65 15.34,-28.17 15.34,-28.17c0,0 -17.22,-18.86 -38.96,-12.6z"/>
<path android:fillColor="#fbbcc9" android:pathData="M458.64,355.09c36.87,27.94 25.88,58.7 46.57,49.92c69.7,-29.55 57.51,-181.21 51.87,-162.87c-31.77,103.41 -100.99,109.2 -167.61,61.63c-13.01,-9.29 48.38,35.57 69.16,51.31z"/>
<path android:fillColor="#fbbcc9" android:pathData="M330.91,303.77c-66.62,47.56 -135.84,41.78 -167.61,-61.63c-5.63,-18.34 -17.82,133.31 51.87,162.87c20.7,8.78 9.7,-21.98 46.57,-49.92c20.78,-15.75 82.17,-60.6 69.16,-51.31z"/>
<path android:fillColor="#3747a9" android:pathData="M465.61,318c80.43,-3.32 95.29,-135.17 88.96,-119.08c-28.39,72.22 -135.86,45.05 -146.13,90.64c-2.02,8.94 18.2,30.06 57.17,28.45z"/>
<path android:fillColor="#3747a9" android:pathData="M311.95,289.55c-10.27,-45.59 -117.75,-18.41 -146.13,-90.64c-6.32,-16.09 8.53,115.76 88.96,119.08c38.97,1.61 59.19,-19.5 57.17,-28.45z"/>
<path android:fillColor="#ff6e40" android:pathData="M403.42,269.47c0,0 43.73,-23.5 81.16,-33.74c34.99,-9.58 61.22,-33.13 64.14,-58.01c2.18,-18.53 -27.05,-53.55 -27.05,-53.55c0,0 -20.51,56.9 -47.41,85.34c-29.28,30.96 -18.15,26.78 -70.84,59.96z"/>
<path android:fillColor="#ff6e40" android:pathData="M246.13,209.51c-26.9,-28.44 -47.41,-85.34 -47.41,-85.34c0,0 -29.23,35.01 -27.05,53.55c2.93,24.88 29.16,48.43 64.14,58.01c37.43,10.25 81.16,33.74 81.16,33.74c-52.69,-33.18 -41.55,-29 -70.84,-59.96z"/>
<path android:fillColor="#ffffff" android:pathData="M398.12,265.85c47.36,-38.85 72.53,-89.54 113.51,-145.02c7.73,-10.46 -34.58,-35.7 -51.31,-37.37c-16.73,-1.67 -30.77,59.79 -32.35,95.94c-1.44,33.01 -36.21,91.68 -29.84,86.45z"/>
<path android:fillColor="#ffffff" android:pathData="M292.42,179.39c-1.58,-36.15 -15.62,-97.61 -32.35,-95.94c-16.73,1.67 -59.04,26.91 -51.31,37.37c40.98,55.48 66.14,106.17 113.51,145.02c6.37,5.22 -28.4,-53.45 -29.84,-86.45z"/>
<path android:fillColor="#ffb327" android:pathData="M402.86,140.35c3.34,-26.76 15.37,-46.32 39.32,-62.75c-21.17,-7.08 -38.77,-12.83 -47.97,-5.3c-9.2,7.53 -34.2,32.7 -30.85,73.68c3.34,40.98 0.18,194.09 7.43,191.25c3.9,-104.87 37.09,-135 32.07,-196.89z"/>
<path android:fillColor="#ffb327" android:pathData="M349.59,337.24c7.24,2.83 4.08,-150.27 7.43,-191.25c3.34,-40.98 -21.65,-66.16 -30.85,-73.68c-9.2,-7.53 -26.8,-1.78 -47.97,5.3c23.95,16.43 35.98,35.98 39.32,62.75c-5.02,61.89 28.17,92.02 32.07,196.89z"/>
</vector>

View file

@ -0,0 +1,18 @@
<vector android:height="48dp" android:viewportHeight="720"
android:viewportWidth="720" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff" android:pathData="M332.48,421.18c0,0 3.77,22.45 -0.82,71.95c-5.76,62.06 23.64,160.64 23.64,160.64c0,0 40.1,-98.78 33.1,-162.59c-5.75,-52.45 2.6,-70.79 0.82,-68.33c-30.81,42.57 -56.75,-1.67 -56.75,-1.67z"/>
<path android:fillColor="#ffffff" android:pathData="M407.6,474.45c5.01,38.77 -0.57,60.01 -7.81,101.51c-3.66,20.99 74.78,-63.1 104.86,-113.23c5.02,-8.36 -28.77,32.6 -62.19,3.35c-23.18,-20.28 -27.16,-26.44 -45.18,-44.06c-6.08,-5.94 6.74,24.72 10.32,52.43z"/>
<path android:fillColor="#ffffff" android:pathData="M321.99,425.09c-18.02,17.62 -22,23.78 -45.18,44.06c-33.42,29.25 -67.21,-11.71 -62.19,-3.35c30.08,50.13 108.52,134.22 104.86,113.23c-7.24,-41.5 -12.82,-62.74 -7.81,-101.51c3.58,-27.71 16.4,-58.37 10.32,-52.43z"/>
<path android:fillColor="#ffffff" android:pathData="M399.15,355.87c36.67,10.57 50.89,61.5 87.91,67.8c7.65,1.3 16.27,3.6 26.31,3.12c18.77,-0.9 42.51,-11.51 74.22,-56.5c9.38,-13.3 -23.27,85.66 -105.13,86.86c-59.96,0.88 -66.97,-58.7 -106.93,-60.51c-14.43,-0.65 -15.34,-28.17 -15.34,-28.17c0,0 17.22,-18.86 38.96,-12.6z"/>
<path android:fillColor="#ffffff" android:pathData="M321.51,355.59c-36.67,10.57 -50.89,61.5 -87.91,67.8c-7.65,1.3 -16.27,3.6 -26.31,3.12c-18.77,-0.9 -42.51,-11.51 -74.22,-56.5c-9.38,-13.3 23.27,85.66 105.13,86.86c59.96,0.88 66.97,-58.7 106.93,-60.51c14.43,-0.65 15.34,-28.17 15.34,-28.17c0,0 -17.22,-18.86 -38.96,-12.6z"/>
<path android:fillColor="#ffffff" android:pathData="M458.64,355.09c36.87,27.94 25.88,58.7 46.57,49.92c69.7,-29.55 57.51,-181.21 51.87,-162.87c-31.77,103.41 -100.99,109.2 -167.61,61.63c-13.01,-9.29 48.38,35.57 69.16,51.31z"/>
<path android:fillColor="#ffffff" android:pathData="M330.91,303.77c-66.62,47.56 -135.84,41.78 -167.61,-61.63c-5.63,-18.34 -17.82,133.31 51.87,162.87c20.7,8.78 9.7,-21.98 46.57,-49.92c20.78,-15.75 82.17,-60.6 69.16,-51.31z"/>
<path android:fillColor="#ffffff" android:pathData="M465.61,318c80.43,-3.32 95.29,-135.17 88.96,-119.08c-28.39,72.22 -135.86,45.05 -146.13,90.64c-2.02,8.94 18.2,30.06 57.17,28.45z"/>
<path android:fillColor="#ffffff" android:pathData="M311.95,289.55c-10.27,-45.59 -117.75,-18.41 -146.13,-90.64c-6.32,-16.09 8.53,115.76 88.96,119.08c38.97,1.61 59.19,-19.5 57.17,-28.45z"/>
<path android:fillColor="#ffffff" android:pathData="M403.42,269.47c0,0 43.73,-23.5 81.16,-33.74c34.99,-9.58 61.22,-33.13 64.14,-58.01c2.18,-18.53 -27.05,-53.55 -27.05,-53.55c0,0 -20.51,56.9 -47.41,85.34c-29.28,30.96 -18.15,26.78 -70.84,59.96z"/>
<path android:fillColor="#ffffff" android:pathData="M246.13,209.51c-26.9,-28.44 -47.41,-85.34 -47.41,-85.34c0,0 -29.23,35.01 -27.05,53.55c2.93,24.88 29.16,48.43 64.14,58.01c37.43,10.25 81.16,33.74 81.16,33.74c-52.69,-33.18 -41.55,-29 -70.84,-59.96z"/>
<path android:fillColor="#ffffff" android:pathData="M398.12,265.85c47.36,-38.85 72.53,-89.54 113.51,-145.02c7.73,-10.46 -34.58,-35.7 -51.31,-37.37c-16.73,-1.67 -30.77,59.79 -32.35,95.94c-1.44,33.01 -36.21,91.68 -29.84,86.45z"/>
<path android:fillColor="#ffffff" android:pathData="M292.42,179.39c-1.58,-36.15 -15.62,-97.61 -32.35,-95.94c-16.73,1.67 -59.04,26.91 -51.31,37.37c40.98,55.48 66.14,106.17 113.51,145.02c6.37,5.22 -28.4,-53.45 -29.84,-86.45z"/>
<path android:fillColor="#ffffff" android:pathData="M402.86,140.35c3.34,-26.76 15.37,-46.32 39.32,-62.75c-21.17,-7.08 -38.77,-12.83 -47.97,-5.3c-9.2,7.53 -34.2,32.7 -30.85,73.68c3.34,40.98 0.18,194.09 7.43,191.25c3.9,-104.87 37.09,-135 32.07,-196.89z"/>
<path android:fillColor="#ffffff" android:pathData="M349.59,337.24c7.24,2.83 4.08,-150.27 7.43,-191.25c3.34,-40.98 -21.65,-66.16 -30.85,-73.68c-9.2,-7.53 -26.8,-1.78 -47.97,5.3c23.95,16.43 35.98,35.98 39.32,62.75c-5.02,61.89 28.17,92.02 32.07,196.89z"/>
</vector>

View file

@ -0,0 +1,18 @@
<vector android:height="108dp" android:viewportHeight="1080"
android:viewportWidth="1080" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#303030" android:pathData="M512.48,601.18c0,0 3.77,22.45 -0.82,71.95c-5.76,62.06 23.64,160.64 23.64,160.64c0,0 40.1,-98.78 33.1,-162.59c-5.75,-52.45 2.6,-70.79 0.82,-68.33c-30.81,42.57 -56.75,-1.67 -56.75,-1.67z"/>
<path android:fillColor="#ffffff" android:pathData="M587.6,654.45c5.01,38.77 -0.57,60.01 -7.81,101.51c-3.66,20.99 74.78,-63.1 104.86,-113.23c5.02,-8.36 -28.77,32.6 -62.19,3.35c-23.18,-20.28 -27.16,-26.44 -45.18,-44.06c-6.08,-5.94 6.74,24.72 10.32,52.43z"/>
<path android:fillColor="#ffffff" android:pathData="M501.99,605.09c-18.02,17.62 -22,23.78 -45.18,44.06c-33.42,29.25 -67.21,-11.71 -62.19,-3.35c30.08,50.13 108.52,134.22 104.86,113.23c-7.24,-41.5 -12.82,-62.74 -7.81,-101.51c3.58,-27.71 16.4,-58.37 10.32,-52.43z"/>
<path android:fillColor="#303030" android:pathData="M579.15,535.87c36.67,10.57 50.89,61.5 87.91,67.8c7.65,1.3 16.27,3.6 26.31,3.12c18.77,-0.9 42.51,-11.51 74.22,-56.5c9.38,-13.3 -23.27,85.66 -105.13,86.86c-59.96,0.88 -66.97,-58.7 -106.93,-60.51c-14.43,-0.65 -15.34,-28.17 -15.34,-28.17c0,0 17.22,-18.86 38.96,-12.6z"/>
<path android:fillColor="#303030" android:pathData="M501.51,535.59c-36.67,10.57 -50.89,61.5 -87.91,67.8c-7.65,1.3 -16.27,3.6 -26.31,3.12c-18.77,-0.9 -42.51,-11.51 -74.22,-56.5c-9.38,-13.3 23.27,85.66 105.13,86.86c59.96,0.88 66.97,-58.7 106.93,-60.51c14.43,-0.65 15.34,-28.17 15.34,-28.17c0,0 -17.22,-18.86 -38.96,-12.6z"/>
<path android:fillColor="#fbbcc9" android:pathData="M638.64,535.09c36.87,27.94 25.88,58.7 46.57,49.92c69.7,-29.55 57.51,-181.21 51.87,-162.87c-31.77,103.41 -100.99,109.2 -167.61,61.63c-13.01,-9.29 48.38,35.57 69.16,51.31z"/>
<path android:fillColor="#fbbcc9" android:pathData="M510.91,483.77c-66.62,47.56 -135.84,41.78 -167.61,-61.63c-5.63,-18.34 -17.82,133.31 51.87,162.87c20.7,8.78 9.7,-21.98 46.57,-49.92c20.78,-15.75 82.17,-60.6 69.16,-51.31z"/>
<path android:fillColor="#3747a9" android:pathData="M645.61,498c80.43,-3.32 95.29,-135.17 88.96,-119.08c-28.39,72.22 -135.86,45.05 -146.13,90.64c-2.02,8.94 18.2,30.06 57.17,28.45z"/>
<path android:fillColor="#3747a9" android:pathData="M491.95,469.55c-10.27,-45.59 -117.75,-18.41 -146.13,-90.64c-6.32,-16.09 8.53,115.76 88.96,119.08c38.97,1.61 59.19,-19.5 57.17,-28.45z"/>
<path android:fillColor="#ff6e40" android:pathData="M583.42,449.47c0,0 43.73,-23.5 81.16,-33.74c34.99,-9.58 61.22,-33.13 64.14,-58.01c2.18,-18.53 -27.05,-53.55 -27.05,-53.55c0,0 -20.51,56.9 -47.41,85.34c-29.28,30.96 -18.15,26.78 -70.84,59.96z"/>
<path android:fillColor="#ff6e40" android:pathData="M426.13,389.51c-26.9,-28.44 -47.41,-85.34 -47.41,-85.34c0,0 -29.23,35.01 -27.05,53.55c2.93,24.88 29.16,48.43 64.14,58.01c37.43,10.25 81.16,33.74 81.16,33.74c-52.69,-33.18 -41.55,-29 -70.84,-59.96z"/>
<path android:fillColor="#ffffff" android:pathData="M578.12,445.85c47.36,-38.85 72.53,-89.54 113.51,-145.02c7.73,-10.46 -34.58,-35.7 -51.31,-37.37c-16.73,-1.67 -30.77,59.79 -32.35,95.94c-1.44,33.01 -36.21,91.68 -29.84,86.45z"/>
<path android:fillColor="#ffffff" android:pathData="M472.42,359.39c-1.58,-36.15 -15.62,-97.61 -32.35,-95.94c-16.73,1.67 -59.04,26.91 -51.31,37.37c40.98,55.48 66.14,106.17 113.51,145.02c6.37,5.22 -28.4,-53.45 -29.84,-86.45z"/>
<path android:fillColor="#ffb327" android:pathData="M582.86,320.35c3.34,-26.76 15.37,-46.32 39.32,-62.75c-21.17,-7.08 -38.77,-12.83 -47.97,-5.3c-9.2,7.53 -34.2,32.7 -30.85,73.68c3.34,40.98 0.18,194.09 7.43,191.25c3.9,-104.87 37.09,-135 32.07,-196.89z"/>
<path android:fillColor="#ffb327" android:pathData="M529.59,517.24c7.24,2.83 4.08,-150.27 7.43,-191.25c3.34,-40.98 -21.65,-66.16 -30.85,-73.68c-9.2,-7.53 -26.8,-1.78 -47.97,5.3c23.95,16.43 35.98,35.98 39.32,62.75c-5.02,61.89 28.17,92.02 32.07,196.89z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M15.386,0.524c-4.764,0 -8.64,3.876 -8.64,8.64 0,4.75 3.876,8.613 8.64,8.613 4.75,0 8.614,-3.864 8.614,-8.613C24,4.4 20.136,0.524 15.386,0.524M0.003,23.537h4.22V0.524H0.003"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M6.908,24L3.804,24c-0.664,0 -1.086,-0.529 -0.936,-1.18l0.149,-0.674h2.071c0.666,0 1.336,-0.533 1.482,-1.182l1.064,-4.592c0.15,-0.648 0.816,-1.18 1.48,-1.18h0.883c3.789,0 6.734,-0.779 8.84,-2.34s3.16,-3.6 3.16,-6.135c0,-1.125 -0.195,-2.055 -0.588,-2.789 0,-0.016 -0.016,-0.031 -0.016,-0.046l0.135,0.075c0.75,0.465 1.32,1.064 1.711,1.814 0.404,0.75 0.598,1.68 0.598,2.791 0,2.535 -1.049,4.574 -3.164,6.135 -2.1,1.545 -5.055,2.324 -8.834,2.324h-0.9c-0.66,0 -1.334,0.525 -1.484,1.186L8.39,22.812c-0.149,0.645 -0.81,1.17 -1.47,1.17L6.908,24zM4.231,21.305L1.126,21.305c-0.663,0 -1.084,-0.529 -0.936,-1.18L4.563,1.182C4.714,0.529 5.378,0 6.044,0h6.465c1.395,0 2.609,0.098 3.648,0.289 1.035,0.189 1.92,0.519 2.684,0.99 0.736,0.465 1.322,1.072 1.697,1.818 0.389,0.748 0.584,1.68 0.584,2.797 0,2.535 -1.051,4.574 -3.164,6.119 -2.1,1.561 -5.056,2.326 -8.836,2.326h-0.883c-0.66,0 -1.328,0.524 -1.478,1.169L5.7,20.097c-0.149,0.646 -0.817,1.172 -1.485,1.172l0.016,0.036zM11.677,3.936h-1.014c-0.666,0 -1.332,0.529 -1.48,1.178l-0.93,4.02c-0.15,0.648 0.27,1.179 0.93,1.179h0.766c1.664,0 2.97,-0.343 3.9,-1.021 0.929,-0.686 1.395,-1.654 1.395,-2.912 0,-0.83 -0.301,-1.445 -0.9,-1.84 -0.6,-0.404 -1.5,-0.605 -2.686,-0.605l0.019,0.001z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/dark"
android:pathData="M5.41,21L6.12,17H2.12L2.47,15H6.47L7.53,9H3.53L3.88,7H7.88L8.59,3H10.59L9.88,7H15.88L16.59,3H18.59L17.88,7H21.88L21.53,9H17.53L16.47,15H20.47L20.12,17H16.12L15.41,21H13.41L14.12,17H8.12L7.41,21H5.41M9.53,9L8.47,15H14.47L15.53,9H9.53Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M23.954,4.569c-0.885,0.389 -1.83,0.654 -2.825,0.775 1.014,-0.611 1.794,-1.574 2.163,-2.723 -0.951,0.555 -2.005,0.959 -3.127,1.184 -0.896,-0.959 -2.173,-1.559 -3.591,-1.559 -2.717,0 -4.92,2.203 -4.92,4.917 0,0.39 0.045,0.765 0.127,1.124C7.691,8.094 4.066,6.13 1.64,3.161c-0.427,0.722 -0.666,1.561 -0.666,2.475 0,1.71 0.87,3.213 2.188,4.096 -0.807,-0.026 -1.566,-0.248 -2.228,-0.616v0.061c0,2.385 1.693,4.374 3.946,4.827 -0.413,0.111 -0.849,0.171 -1.296,0.171 -0.314,0 -0.615,-0.03 -0.916,-0.086 0.631,1.953 2.445,3.377 4.604,3.417 -1.68,1.319 -3.809,2.105 -6.102,2.105 -0.39,0 -0.779,-0.023 -1.17,-0.067 2.189,1.394 4.768,2.209 7.557,2.209 9.054,0 13.999,-7.496 13.999,-13.986 0,-0.209 0,-0.42 -0.015,-0.63 0.961,-0.689 1.8,-1.56 2.46,-2.548l-0.047,-0.02z"/>
</vector>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="@color/su_request_background"/>
</shape>
</item>
<item>
<inset
android:drawable="@drawable/ic_extension"
android:inset="13dp"/>
</item>
</layer-list>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="@color/su_request_background"/>
</shape>
</item>
<item>
<inset
android:drawable="@drawable/ic_superuser"
android:inset="13dp"/>
</item>
</layer-list>

View file

@ -0,0 +1,190 @@
<resources>
<!--Sections-->
<string name="modules">الإضافات</string>
<string name="superuser">صلاحية الروت</string>
<string name="logs">السجلات</string>
<string name="settings">الإعدادات</string>
<string name="install">تثبيت</string>
<string name="section_home">الصفحة الرئيسية</string>
<string name="section_theme">المظهر</string>
<!--Home-->
<string name="no_connection">لا يوجد إتصال</string>
<string name="app_changelog">تفاصيل التحديث</string>
<string name="loading">جارٍ التحميل...</string>
<string name="update">تحديث</string>
<string name="not_available">غير متوفر</string>
<string name="hide">إخفاء</string>
<string name="home_package">الحزمة</string>
<string name="home_support_title">تبرع لنا</string>
<string name="home_item_source">الكود المصدري للتطبيق</string>
<string name="home_support_content">ماجيسك هو، وسيظل دوماً، مجانياّ و مفتوح المصدر، اظهر اهتمامك لنا لكي نبقيه هكذا بدعم مالي صغير</string>
<string name="home_installed_version">تم التثبيت</string>
<string name="home_latest_version">آخر إصدار</string>
<string name="invalid_update_channel">مصدر التحديث غير صالح</string>
<string name="uninstall_magisk_title">إلغاء تثبيت ماجيسك</string>
<string name="uninstall_magisk_msg">ستُعطل/ستُحذف جميع الإضافات. سيُحذف الروت، وربما ستشفر بياناتك إذا لم تكن غير مشفرة حالياً.</string>
<!--Install-->
<string name="keep_force_encryption">فرض التشفير الإجباري</string>
<string name="keep_dm_verity">فرض تشفيرات AVB2.0/dm-verity</string>
<string name="recovery_mode">وضـع الريكفري</string>
<string name="install_options_title">الخيارات</string>
<string name="install_method_title">الطريقة</string>
<string name="install_next">التالي</string>
<string name="install_start">هيا بنا</string>
<string name="manager_download_install">اضغط للتنزيل و التثبيت</string>
<string name="direct_install">تثبيت مباشر (موصى بها)</string>
<string name="install_inactive_slot">التثبيت على المنطقة الغير نشطة (بعد OTA)</string>
<string name="install_inactive_slot_msg">سيُجبر جهازك للاقلاع على المنطقة الغير النشطة بعد إعادة التشغيل!\n استخدم هذا الخيار فقط بعد الانتهاء من OTA. استمرار؟</string>
<string name="setup_title">إعدادات إضافية</string>
<string name="select_patch_file">حدد و عدل ملفاً</string>
<string name="patch_file_msg">اختر ملف (*.img) أو ملف odin (*.tar)</string>
<string name="reboot_delay_toast">إعادة التشغيل بعد ٥ ثواني…</string>
<string name="flash_screen_title">التثبيت</string>
<!--Superuser-->
<string name="su_request_title">طلبات صلاحية الروت</string>
<string name="deny">رفض</string>
<string name="prompt">طلب</string>
<string name="grant">سماح</string>
<string name="su_warning">يمنح حق الوصول الكامل إلى جهازك. ارفض إذا كنت غير متأكد!</string>
<string name="forever">للأبد</string>
<string name="once">مرة واحدة</string>
<string name="tenmin">10 دقائق</string>
<string name="twentymin">20 دقيقة</string>
<string name="thirtymin">30 دقيقة</string>
<string name="sixtymin">60 دقيقة</string>
<string name="su_allow_toast">تم منح صلاحيات الروت لـ%1$s</string>
<string name="su_deny_toast">تم رفض صلاحيات الروت لـ%1$s </string>
<string name="su_snack_grant">تم منح صلاحيات الروت لـ%1$s</string>
<string name="su_snack_deny">تم رفض صلاحيات الروت لـ%1$s</string>
<string name="su_snack_notif_on">تم تفعيل الإشعارات لـ%1$s</string>
<string name="su_snack_notif_off">تم تعطيل الإشعارات لـ%1$s</string>
<string name="su_snack_log_on">تم تفعيل السجلات لـ%1$s</string>
<string name="su_snack_log_off">تم تعطيل السجلات لـ%1$s</string>
<string name="su_revoke_title">منع؟</string>
<string name="su_revoke_msg">هل تريد منع صلاحية %1$s?</string>
<string name="toast">اشعار</string>
<string name="none">بدون</string>
<string name="superuser_toggle_notification">الإشعارات</string>
<string name="superuser_toggle_revoke">منع</string>
<string name="superuser_policy_none">لم يسأل آية تطبيق لصلاحيات الروت</string>
<!--Logs-->
<string name="log_data_none">لا يوجد أية سجلات هنا :-:، حاول استعمال تطبيقات روت اكثر</string>
<string name="log_data_magisk_none">لا توجد أيةَ سجلات ⊙_⊙</string>
<string name="menuSaveLog">حفظ السجلات</string>
<string name="menuClearLog">حذف السجلات</string>
<string name="logs_cleared">تم الحذف بنجاح</string>
<string name="pid">PID: %1$d</string>
<string name="target_uid">UID: %1$d</string>
<!--SafetyNet-->
<!-- MagiskHide -->
<string name="show_system_app">إظهار برامج النظام</string>
<string name="hide_filter_hint">البحث بالاسم</string>
<string name="hide_search">ابحث</string>
<!--Module Fragment-->
<string name="no_info_provided">(لا تتوفر معلومات)</string>
<string name="reboot_recovery">إعادة التشغيل إلى Recovery</string>
<string name="reboot_bootloader">إعادة التشغيل إلى Bootloader</string>
<string name="reboot_download">إعادة التشغيل إلى وضـع Odin</string>
<string name="reboot_edl">إعادة التشغيل إلى EDL</string>
<string name="module_version_author">%1$sبواسطة%2$s</string>
<string name="module_state_remove">إزالة </string>
<string name="module_state_restore">إسترجاع</string>
<string name="module_action_install_external">اختر من الذاكرة الداخلية</string>
<string name="update_available">التحديث متوفر</string>
<string name="external_rw_permission_denied">امنحني إذن الولوج للذاكرة الداخلية</string>
<!--Settings -->
<string name="settings_dark_mode_title">المظهر</string>
<string name="settings_dark_mode_message">حدد المظهر الذي يناسب ذوقك</string>
<string name="settings_dark_mode_light">الوضـع المضيء</string>
<string name="settings_dark_mode_system">اتبّع النظام</string>
<string name="settings_dark_mode_dark">الوضع المظلم</string>
<string name="settings_download_path_title">مسار التحميل</string>
<string name="settings_download_path_message">ستحمل الملفات إلى %1$s</string>
<string name="language">اللغة</string>
<string name="system_default">(الإفتراضي)</string>
<string name="settings_check_update_title">تحقق من التحديثات</string>
<string name="settings_check_update_summary">التحقق من التحديثات في الخلفية بشكل دوري</string>
<string name="settings_update_channel_title">مصدر التحديثات</string>
<string name="settings_update_stable">مستقر</string>
<string name="settings_update_beta">تجريبي</string>
<string name="settings_update_custom">مخصص</string>
<string name="settings_update_custom_msg">أدخل الرابط لمصدرك المخصص</string>
<string name="settings_hosts_title">موانع الاعلانات</string>
<string name="settings_hosts_summary">حجب الاعلانات دون تعديل النظام</string>
<string name="settings_hosts_toast">تم تمكين خاصية حجب الاعلانات</string>
<string name="settings_app_name_hint">الاسم الجديد</string>
<string name="settings_app_name_helper">التطبيق الجديد سوف يملك هذا الاسم</string>
<string name="settings_app_name_error">الصيغة غير مقبولة</string>
<string name="settings_su_app_adb">التطبيقات و ADB</string>
<string name="settings_su_app">التطبيقات فقط</string>
<string name="settings_su_adb">ADB فقط</string>
<string name="settings_su_disable">معطل</string>
<string name="settings_su_request_10">10 ثواني</string>
<string name="settings_su_request_15">15 ثانية</string>
<string name="settings_su_request_20">20 ثانية</string>
<string name="settings_su_request_30">30 ثانية</string>
<string name="settings_su_request_45">45 ثانية</string>
<string name="settings_su_request_60">60 ثانية</string>
<string name="superuser_access">صلاحيات الروت</string>
<string name="auto_response">الفعل التلقائي</string>
<string name="request_timeout">المهلة قبل الفعل التلقائي</string>
<string name="superuser_notification">إشعارات طلبات الروت</string>
<string name="settings_su_reauth_title">إعادة المصادقة بعد التحديث</string>
<string name="settings_su_reauth_summary">أعد مصادقة صلاحيات الروت بعد تحديث التطبيق</string>
<string name="settings_customization">تخصيص</string>
<string name="multiuser_mode">نمط المستخدم المزدوج</string>
<string name="settings_owner_only">مالك الجهاز فقط</string>
<string name="settings_owner_manage">المالك هو من يحدد</string>
<string name="settings_user_independent">مستقل</string>
<string name="owner_only_summary">للمالك فقط له صلاحيات الروت</string>
<string name="owner_manage_summary">فقط المالك من يرفض و يمنح صلاحيات الروت</string>
<string name="user_independent_summary">كل مستخدم له قواعد روت خاصة به</string>
<string name="mount_namespace_mode">نمط Mount Namespace</string>
<string name="settings_ns_global">نمط Namespace عام</string>
<string name="settings_ns_requester">نمط NameSpace متوارث</string>
<string name="settings_ns_isolate">نمط NameSpace معزول</string>
<string name="global_summary">جميع الجلسات الروت تستخدم NameSpace العام</string>
<string name="requester_summary">جميع الجلسات الروت تستخدم NameSpace المتوارث</string>
<string name="isolate_summary">جميع الجلسات الروت تستخدم NameSpace المعزول</string>
<!--Notifications-->
<string name="update_channel">تحديثات ماجيسك</string>
<string name="progress_channel">إشعارات التقدم</string>
<string name="download_complete">اكتمل التنزيل</string>
<string name="download_file_error">فشل تنزيل الملف</string>
<string name="magisk_update_title">تحديث ماجيسك متوفر!</string>
<!--Toasts, Dialogs-->
<string name="yes">نعم</string>
<string name="no">لا</string>
<string name="download">تنزيل</string>
<string name="reboot">إعادة التشغيل</string>
<string name="release_notes">معلومات الإصدار الجديد</string>
<string name="flashing">يتم التثبيت...</string>
<string name="done">تم!</string>
<string name="failure">فشل!</string>
<string name="open_link_failed_toast">لم يُعثر على تطبيق لفتح الرابط …</string>
<string name="complete_uninstall">إلغاء التثبيت بالكامل</string>
<string name="restore_img">استعادة الصور</string>
<string name="restore_img_msg">جار الإستعادة…</string>
<string name="restore_done">تم الإستعادة</string>
<string name="restore_fail">النسخة الإحتياطية الأصلية غير موجودة!</string>
<string name="setup_fail">فشل الإعداد</string>
<string name="env_fix_title">الإعداد الإضافي مطلوب</string>
<string name="setup_msg">جار إعداد البيئة</string>
<string name="unsupport_magisk_title">إصدار ماجيسك غير مدعوم</string>
</resources>

View file

@ -0,0 +1,236 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Sections-->
<string name="modules">Módulos</string>
<string name="superuser">Superusuariu</string>
<string name="logs">Rexistru</string>
<string name="settings">Configuración</string>
<string name="install">Instalar</string>
<string name="section_home">Aniciu</string>
<string name="section_theme">Estilos</string>
<string name="denylist">Llista d\'esclusión</string>
<!--Home-->
<string name="no_connection">L\'accesu a internet nun ta disponible</string>
<string name="app_changelog">Rexistru de cambeos</string>
<string name="loading">Cargando…</string>
<string name="update">Anovar</string>
<string name="not_available">N/D</string>
<string name="hide">Esconder</string>
<string name="home_package">Paquete</string>
<string name="home_app_title">Aplicación</string>
<string name="home_notice_content">Baxa Magisk NAMÁS dende la páxina oficial de GitHub. ¡Los ficheros de fontes desconocíes puen ser maliciosos!</string>
<string name="home_support_title">Collaboración</string>
<string name="home_follow_title">Redes sociales</string>
<string name="home_item_source">Códigu fonte</string>
<string name="home_support_content">Magisk ye y va ser de códigu abiertu y gratuitu. Sicasí, pues ayudanos faciendo una donación o collaborando.</string>
<string name="home_installed_version">Versión instalada</string>
<string name="home_latest_version">Última versión</string>
<string name="invalid_update_channel">La canal d\'anovamientu nun ye válida</string>
<string name="uninstall_magisk_title">Desinstalar Magisk</string>
<string name="uninstall_magisk_msg">¡Van quitase y desactivase tolos módulos y l\'accesu root!\nCualesquier almacenamientu internu ensin cifrar pente l\'usu de Magisk va volver cifrase.</string>
<!--Install-->
<string name="keep_force_encryption">Caltener el cifráu forciáu</string>
<string name="keep_dm_verity">Caltener l\'AVB 2.0/dm-verity</string>
<string name="recovery_mode">Mou de recuperación</string>
<string name="install_options_title">Opciones</string>
<string name="install_method_title">Métodu</string>
<string name="install_next">Siguiente</string>
<string name="install_start">Siguir</string>
<string name="manager_download_install">Primi equí pa baxalu ya instalalu</string>
<string name="direct_install">Instalación direuta (aconséyase)</string>
<string name="install_inactive_slot">Instalar na ralura inactiva (darréu del OTA)</string>
<string name="install_inactive_slot_msg">¡El preséu va arrincar OBLIGATORIAMENTE na ralura inactiva dempués de reaniciar!\nUsa esta opción namás dempués d\'acabar l\'anovamientu per OTA.\n¿Quies siguir?</string>
<string name="setup_title">Configuración adicional</string>
<string name="select_patch_file">Seleicionar y parchiar un ficheru</string>
<string name="patch_file_msg">Seleiciona una imaxe en bruto (*.img), un archivu d\'ODIN (*.tar) o un ficheru payload.bin (*.bin)</string>
<string name="reboot_delay_toast">Reaniciando en 5 segundos…</string>
<string name="flash_screen_title">Instalación</string>
<!--Superuser-->
<string name="su_request_title">Solicitú de superusuariu</string>
<string name="touch_filtered_warning">Como una aplicación torga la solicitú de superusuariu, Magisk nun pue verificar la to rempuesta</string>
<string name="deny">Negar</string>
<string name="prompt">Entrugar</string>
<string name="grant">Conceder</string>
<string name="su_warning">Concede l\'accesu completu al preséu.\n¡Niégalu en casu de dulda!</string>
<string name="forever">Siempres</string>
<string name="once">Una vegada</string>
<string name="tenmin">10 minutos</string>
<string name="twentymin">20 minutos</string>
<string name="thirtymin">30 minutos</string>
<string name="sixtymin">60 minutos</string>
<string name="su_allow_toast">A %1$s concediéronse-y los derechos de superusuariu</string>
<string name="su_deny_toast">A %1$s negáronse-y los derechos de superusuariu</string>
<string name="su_snack_grant">Concediéronse los derechos de superusuariu a %1$s</string>
<string name="su_snack_deny">Negáronse los derechos de superusuariu a %1$s</string>
<string name="su_snack_notif_on">Activáronse los avisos de: %1$s</string>
<string name="su_snack_notif_off">Desactiváronse los avisos de: %1$s</string>
<string name="su_snack_log_on">Activóse\'l rexistru de: %1$s</string>
<string name="su_snack_log_off">Desactivóse\'l rexistru de: %1$s</string>
<string name="su_revoke_title">Revocación de derechos</string>
<string name="su_revoke_msg">Confirma la revocación de los derechos de superusuariu pa «%1$s»</string>
<string name="toast">Burbuya d\'avisu emerxente</string>
<string name="none">Nada</string>
<string name="superuser_toggle_notification">Avisos</string>
<string name="superuser_toggle_revoke">Revocar</string>
<string name="superuser_policy_none">Pel momentu, nenguna aplicación pidió\'l permisu de superusuariu.</string>
<!--Logs-->
<string name="log_data_none">El rexistru ta baleru. Prueba a usar más aplicaciones de root</string>
<string name="log_data_magisk_none">El rexistru de Magisk ta baleru. ¡Qué raro!</string>
<string name="menuSaveLog">Guardar el rexistru</string>
<string name="menuClearLog">Llimpiar el rexistru</string>
<string name="logs_cleared">El rexistru borróse correutamente</string>
<string name="pid">PID: %1$d</string>
<string name="target_uid">UID de destín: %1$d</string>
<string name="target_pid">Mount ns target PID: %s</string>
<string name="selinux_context">Contestu de SELinux: %s</string>
<string name="supp_group">Grupu suplementariu: %s</string>
<!--SafetyNet-->
<!--MagiskHide-->
<string name="show_system_app">Aplicaciones del sistema</string>
<string name="show_os_app">Aplicaciones del SO</string>
<string name="hide_filter_hint">Peñerar pol nome</string>
<string name="hide_search">Buscar</string>
<!--Module-->
<string name="no_info_provided">(Nun s\'apurrió nenguna información)</string>
<string name="reboot_userspace">Reaniciu del sistema</string>
<string name="reboot_recovery">Reaniciar al recovery</string>
<string name="reboot_bootloader">Reaniciar al cargador d\'arrinque</string>
<string name="reboot_download">Reaniciar al mou de descarga</string>
<string name="reboot_edl">Reaniciar al mou EDL</string>
<string name="reboot_safe_mode">Mou seguru</string>
<string name="module_version_author">%1$s por %2$s</string>
<string name="module_state_remove">Quitar</string>
<string name="module_action">Aición</string>
<string name="module_state_restore">Restaurar</string>
<string name="module_action_install_external">Instalar dende l\'almacenamientu</string>
<string name="update_available">Hai un anovamientu disponible</string>
<string name="suspend_text_riru">Suspendióse\'l módulu porque s\'activó «%1$s»</string>
<string name="suspend_text_zygisk">Suspendióse\'l módulu porque nun s\'activó «%1$s»</string>
<string name="zygisk_module_unloaded">El módulu de Zygisk nun cargó por haber incompatibilidaes</string>
<string name="module_empty">Nun hai nengún módulu instaláu</string>
<string name="confirm_install">¿Quies instalar el módulu «%1$s»?</string>
<string name="confirm_install_title">Confirmación de la instalación</string>
<!--Settings-->
<string name="settings_dark_mode_title">Mou del estilu</string>
<string name="settings_dark_mode_message">¡Seleiciona\'l mou que meyor s\'adaute al to estilu!</string>
<string name="settings_dark_mode_light">Claridá</string>
<string name="settings_dark_mode_system">L\'estilu del sistema</string>
<string name="settings_dark_mode_dark">Escuridá</string>
<string name="settings_download_path_title">Camín de les descargues</string>
<string name="settings_download_path_message">Los ficheros van guardase en «%1$s»</string>
<string name="settings_hide_app_title">Esconder Magisk</string>
<string name="settings_hide_app_summary">Instala una aplicación intermedia con una ID y una etiqueta al debalu</string>
<string name="settings_restore_app_title">Restaurar el mou visible</string>
<string name="settings_restore_app_summary">Fai que l\'aplicación orixinal vuelva ser visible</string>
<string name="language">Llingua</string>
<string name="system_default">(Lo predeterminao)</string>
<string name="settings_check_update_title">Comprobación d\'anovamientos</string>
<string name="settings_check_update_summary">Comprueba si hai anovamientos en segundu planu</string>
<string name="settings_update_channel_title">Canal d\'anovamientu</string>
<string name="settings_update_stable">Estable</string>
<string name="settings_update_beta">Beta</string>
<string name="settings_update_custom">Canal personalizada</string>
<string name="settings_update_custom_msg">Inxerta la URL d\'una canal personalizada</string>
<string name="settings_zygisk_summary">Executa partes de Magisk nel degorriu de Zygote</string>
<string name="settings_denylist_title">Forciar la llista d\'esclusión</string>
<string name="settings_denylist_summary">Los procesos de la llista d\'esclusión tienen toles modificaciones de Magisk anulaes</string>
<string name="settings_denylist_config_title">Configurar la llista d\'esclusión</string>
<string name="settings_denylist_config_summary">Seleiciona los procesos que s\'inclúin na llista d\'esclusión</string>
<string name="settings_hosts_title">Módulu «Systemless Hosts»</string>
<string name="settings_hosts_summary">Un módulu pa les aplicaciones que bloquien anuncios</string>
<string name="settings_hosts_toast">Amestóse\'l módulu «Systemless Hosts»</string>
<string name="settings_app_name_hint">Nome nuevu</string>
<string name="settings_app_name_helper">L\'aplicación va volver empaquetase con esti nome</string>
<string name="settings_app_name_error">El formatu nun ye válidu</string>
<string name="settings_su_app_adb">Aplicaciones y ADB</string>
<string name="settings_su_app">Namás aplicaciones</string>
<string name="settings_su_adb">Namás ADB</string>
<string name="settings_su_disable">Non</string>
<string name="settings_su_request_10">10 segundos</string>
<string name="settings_su_request_15">15 segundos</string>
<string name="settings_su_request_20">20 segundos</string>
<string name="settings_su_request_30">30 segundos</string>
<string name="settings_su_request_45">45 segundos</string>
<string name="settings_su_request_60">60 segundos</string>
<string name="superuser_access">Accesu de superusuariu</string>
<string name="auto_response">Rempuesta automática</string>
<string name="request_timeout">Tiempu d\'espera de les solicitúes</string>
<string name="superuser_notification">Avisu de superusuariu</string>
<string name="settings_su_reauth_title">Volver autenticar dempués d\'anovar</string>
<string name="settings_su_reauth_summary">Vuelve pidir los permisos de superusuariu dempués d\'anovar les aplicaciones</string>
<string name="settings_su_tapjack_title">Proteición escontra\'l tapjacking</string>
<string name="settings_su_tapjack_summary">El diálogu de concesión de permisos de superusuariu nun respuende a la entrada mentanto lu torgue otra ventana o superposición</string>
<string name="settings_su_auth_title">Autenticación d\'usuariu</string>
<string name="settings_su_auth_summary">Pide l\'autenticación demientres les solicitúes de superusuariu</string>
<string name="settings_su_auth_insecure">Nun se configuró nengún métodu d\'autenticación nel preséu</string>
<string name="settings_customization">Personalización</string>
<string name="setting_add_shortcut_summary">Amiesta un atayu a la pantalla d\'aniciu en casu de que\'l nome y l\'iconu seyan difíciles de reconocer dempués d\'esconder l\'aplicación</string>
<string name="settings_doh_title">DNS per HTTPS</string>
<string name="settings_doh_description">Una igua alternativa pal envelenamientu de DNS en dalgunos países</string>
<string name="settings_random_name_title">Nome de la salida aleatoriu</string>
<string name="settings_random_name_description">Fai que\'l nome de ficheru de la salida de les imáxenes parchiaes y los ficheros tar seya aleatoriu pa impidir la deteición</string>
<string name="multiuser_mode">Mou de multiusuariu</string>
<string name="settings_owner_only">Namás el propietariu del preséu</string>
<string name="settings_owner_manage">El propietariu xestionáu del preséu</string>
<string name="settings_user_independent">Con independencia del usuariu</string>
<string name="owner_only_summary">Namás el propietariu tien accesu de root</string>
<string name="owner_manage_summary">Namás el propietariu pue xestionar l\'accesu de root y recibir solicitúes</string>
<string name="user_independent_summary">Cada usuariu tien les sos regles de root individuales</string>
<string name="mount_namespace_mode">Mou del montaxe del espaciu de nomes</string>
<string name="settings_ns_global">Espaciu de nomes global</string>
<string name="settings_ns_requester">Espaciu de nomes heredáu</string>
<string name="settings_ns_isolate">Espaciu de nomes aisláu</string>
<string name="global_summary">Toles sesiones de root usen l\'espaciu de nomes global</string>
<string name="requester_summary">Les sesiones de root herieden l\'espaciu de nomes de los solicitantes</string>
<string name="isolate_summary">Cada sesión de root tien el so espaciu de nomes propiu</string>
<!--Notifications-->
<string name="update_channel">Anovamientos de Magisk</string>
<string name="progress_channel">Avisos de progresos</string>
<string name="updated_channel">Anovamientu completáu</string>
<string name="download_complete">Completóse la descarga</string>
<string name="download_file_error">Hebo un fallu al baxar el ficheru</string>
<string name="magisk_update_title">¡Hai un anovamientu pa Magisk!</string>
<string name="updated_title">Magisk anovóse</string>
<string name="updated_text">Toca equí p\'abrir l\'aplicación</string>
<!--Toasts, Dialogs-->
<string name="yes"></string>
<string name="no">Non</string>
<string name="repo_install_title">Instalación de: %1$s %2$s (%3$d)</string>
<string name="download">Baxar</string>
<string name="reboot">Reaniciar</string>
<string name="close">Zarrar</string>
<string name="release_notes">Notes de la versión</string>
<string name="flashing">Flaxando…</string>
<string name="running">Executando…</string>
<string name="done">¡Fecho!</string>
<string name="done_action">Completóse l\'aición de: %1$s</string>
<string name="failure">¡Falló!</string>
<string name="hide_app_title">Escondiendo l\'aplicación Magisk…</string>
<string name="open_link_failed_toast">Nun s\'atopó nenguna aplicación p\'abrir l\'enllaz</string>
<string name="complete_uninstall">Desinstalar dafechu</string>
<string name="restore_img">Restaurar les imáxenes</string>
<string name="restore_img_msg">Restaurando…</string>
<string name="restore_done">¡Restauración fecha!</string>
<string name="restore_fail">¡La copia de seguranza de la imaxe de fábrica nun esiste!</string>
<string name="setup_fail">La configuración falló</string>
<string name="env_fix_title">Configuración adicional</string>
<string name="env_fix_msg">El preséu precisa una configuración adicional pa que Magisk funcione afayadizamente. ¿Quies siguir y reaniciar?</string>
<string name="env_full_fix_msg">El preséu precisa volver flaxar Magisk pa que funcione afayadizamente. Volvi instalar Magisk dientro de l\'aplicación porque\'l mou de recuperación nun pue consiguir la información correuta del preséu.</string>
<string name="setup_msg">Executando la configuración del entornu…</string>
<string name="unsupport_magisk_title">Versión non compatible</string>
<string name="unsupport_magisk_msg">Esta versión de l\'aplicación nun ye compatible coles versiones de Magisk anteriores a la %1$s.\n\nL\'aplicación va comportase como si Magisk nun tuviere instaláu, anueva Magisk namás que puedas.</string>
<string name="unsupport_general_title">Estáu anormal</string>
<string name="unsupport_system_app_msg">Esta aplicación nun se pue executar nel espaciu del sistema. Volvi instalala mas nel espaciu del usuariu.</string>
<string name="unsupport_other_su_msg">Detectóse un binariu «su» que nun ye de Magisk. Quita cualesquier solución de root y/o volvi instalar Magisk.</string>
<string name="unsupport_external_storage_msg">Magisk ta instaláu nel almacenamientu esternu. Movi l\'aplicación al almacenamientu internu, por favor.</string>
<string name="unsupport_nonroot_stub_msg">Magisk nun pue siguir funcionando nel mou escondíu darréu que se perdió\'l root. Restaura\'l mou visible de l\'aplicación.</string>
<string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string>
<string name="external_rw_permission_denied">Concede\'l permisu d\'almacenamientu p\'activar esta funcionalidá</string>
<string name="post_notifications_denied">Concede\'l permisu de los avisos p\'activar esta función</string>
<string name="install_unknown_denied">Permite la instalación d\'aplicaciones desconocíes p\'activar esta funcionalidá</string>
<string name="add_shortcut_title">Amestar un atayu a la pantalla d\'aniciu</string>
<string name="add_shortcut_msg">Dempués d\'esconder esta aplicación, el so nome ya iconu van ser difíciles de reconocer. ¿Quies amestar un atayu a la pantalla d\'aniciu?</string>
<string name="app_not_found">Nun s\'atopó nenguna aplicación pa remanar esta aición</string>
<string name="reboot_apply_change">Reanicia p\'aplicar los cambeos</string>
<string name="restore_app_confirmation">Esta aición va restaurar l\'aplicación orixinal y desanicia la intermedia. ¿De xuru que quies facelo?</string>
</resources>

View file

@ -0,0 +1,239 @@
<resources>
<!--Sections-->
<string name="modules">Modullar</string>
<string name="superuser">Super İstifadəçi</string>
<string name="logs">Qeydlər</string>
<string name="settings">Ayarlar</string>
<string name="install">Quraşdır</string>
<string name="section_home">Əsas Səhifə</string>
<string name="section_theme">Mövzular</string>
<string name="denylist">Rəddedilənlər siyahısı</string>
<!--Home-->
<string name="no_connection">Bağlantı yoxdur</string>
<string name="app_changelog">Dəyişikliklər</string>
<string name="loading">Yüklənir…</string>
<string name="update">Yenilə</string>
<string name="not_available">Yüklü deyil</string>
<string name="hide">Gizlə</string>
<string name="home_package">Paket</string>
<string name="home_app_title">Tətbiq</string>
<string name="home_notice_content">Magiski YALNIZ rəsmi GitHub səhifəsindən endirin. Bilinməyən mənbələrdəki fayllar zərərli ola bilər!</string>
<string name="home_support_title">Bizə Dəstək ol</string>
<string name="home_follow_title">Bizi İzləyin</string>
<string name="home_item_source">Mənbə</string>
<string name="home_support_content">Magisk pulsuzdur və həmişə belə qalacaq. Amma istəsən bizə dəstək ola bilərsən.</string>
<string name="home_installed_version">Qurulan</string>
<string name="home_latest_version">Ən son versiya</string>
<string name="invalid_update_channel">Keçərsiz yeniləmə kanalı</string>
<string name="uninstall_magisk_title">Magisk\'i sil</string>
<string name="uninstall_magisk_msg">Bütün modullar ləğv olunacaq/silinəcək. Root silinəcək, və əgər hal-hazırda deyilsə, bütün məlumatlarınız potensiyal olaraq şifrələnəcək.</string>
<!--Install-->
<string name="keep_force_encryption">Zorla şifrələməni saxla</string>
<string name="keep_dm_verity">AVB 2.0/dm-verity saxla</string>
<string name="recovery_mode">Xilasetmə Modu</string>
<string name="install_options_title">Ayarlar</string>
<string name="install_method_title">Metod</string>
<string name="install_next">Növbəti</string>
<string name="install_start">Başlat</string>
<string name="manager_download_install">Endirmək və quraşdırmaq üçün bas</string>
<string name="direct_install">Birbaşa Quraşdır (Tövsiyyə olunur)</string>
<string name="install_inactive_slot">Aktiv olmayan Slot\'a quraşdır (OTA\'dan sonra)</string>
<string name="install_inactive_slot_msg">Cihazınız yenidən başladıqdan sonra aktiv olmayan slotda başlamağa MƏCBUR ediləcək!\nBu seçimi ancaq OTA bitdikdən sonra tətbiq edin.\nDavam edilsin?</string>
<string name="setup_title">Əlavə Sazlamalar</string>
<string name="select_patch_file">Fayl seç və yamaqla</string>
<string name="patch_file_msg">(*.img) yaxud ODIN (*.tar) seç</string>
<string name="reboot_delay_toast">5 saniyəyə yenidən başlayacaq…</string>
<string name="flash_screen_title">Quraşdırılma</string>
<!--Superuser-->
<string name="su_request_title">Super İstifadəçi İcazəsi</string>
<string name="touch_filtered_warning">Hansısa tətbiq Super İstifadəçi icazəsini pozduğuna görə, Magisk göstərişinizi yerinə yetirə bilmir</string>
<string name="deny">İmtina et</string>
<string name="prompt">İstək</string>
<string name="grant">İcazə ver</string>
<string name="su_warning">Bu tətbiqə tam səlahiyyət verəcək.\nƏmin deyilsənsə imtina et!</string>
<string name="forever">Həmişə</string>
<string name="once">1 dəfəlik</string>
<string name="tenmin">10 dəqiqəlik</string>
<string name="twentymin">20 dəqiqəlik</string>
<string name="thirtymin">30 dəqiqəlik</string>
<string name="sixtymin">60 dəqiqəlik</string>
<string name="su_allow_toast">%1$s Super İstifadəçi icazəsi aldı</string>
<string name="su_deny_toast">%1$s Super İstifadəçi icazəsi ala bilmədi</string>
<string name="su_snack_grant">%1$s üçün Super İstifadəçi haqları verilib</string>
<string name="su_snack_deny">%1$s üçün Super İstifadəçi haqları verilməyib</string>
<string name="su_snack_notif_on">%1$s üçün bildirişlər açıqdır</string>
<string name="su_snack_notif_off">%1$s üçün bildirişlər qapalıdır</string>
<string name="su_snack_log_on">%1$s qeydləri açıqdır</string>
<string name="su_snack_log_off">%1$s qeydləri qapalıdır</string>
<string name="su_revoke_title">Sil</string>
<string name="su_revoke_msg">%1$s üçün Super İstifadəçi haqlarının silinməsini təsdiqlə</string>
<string name="toast">Tost</string>
<string name="none">Heçbiri</string>
<string name="superuser_toggle_notification">bildirişlər</string>
<string name="superuser_toggle_revoke">Sil</string>
<string name="superuser_policy_none">Hələ heçbir tətbiq Super İstifadəçi icazəsi istəməyib.</string>
<!--Logs-->
<string name="log_data_none">Qeyd yoxdur, root icazəli tətbiqlərdən daha çox istifadə edin</string>
<string name="log_data_magisk_none">Qəribədir, Magisk qeydləri boşdur</string>
<string name="menuSaveLog">Qeydləri saxla</string>
<string name="menuClearLog">Qeydləri sil</string>
<string name="logs_cleared">Qeydlər silindi</string>
<string name="pid">PID: %1$d</string>
<string name="target_uid">Hədəf UID: %1$d</string>
<string name="selinux_context">SELinux kontenti: %s</string>
<string name="supp_group">Əlavə grup: %s</string>
<!--SafetyNet-->
<!--MagiskHide-->
<string name="show_system_app">Sistem tətbiqlərini göstər</string>
<string name="show_os_app">Əməliyyat sistemi tətbiqlərini göstər</string>
<string name="hide_filter_hint">Adla sırala</string>
<string name="hide_search">Axtar</string>
<!--Module-->
<string name="no_info_provided">(Məlumat verilməyib)</string>
<string name="reboot_userspace">Yumuşaq yenidən başlat</string>
<string name="reboot_recovery">Xilasetmə modunda yenidən başlat</string>
<string name="reboot_bootloader">Önyükləyici modunda yenidən başlat</string>
<string name="reboot_download">Yükləmə modunda yenidən başlat</string>
<string name="reboot_edl">EDL modunda yenidən başlat</string>
<string name="module_version_author">%2$s tərəfindən %1$s buraxılışı</string>
<string name="module_state_remove">Sil</string>
<string name="module_state_restore">Bərpa et</string>
<string name="module_action_install_external">Yaddaşdan yüklə</string>
<string name="update_available">Yeniləmə Mövcuddur</string>
<string name="suspend_text_riru">%1$s açıq olduğundan Magisk modulu dayandırıldı</string>
<string name="suspend_text_zygisk">%1$s qapalı olduğu üçün Magisk modulu dayandırıldı</string>
<string name="zygisk_module_unloaded">Zygisk modulu uyğunsuzluq səbəbindən açılmadı</string>
<string name="module_empty">Yüklü modul yoxdur</string>
<string name="confirm_install">%1$s modulu yüklənsin?</string>
<string name="confirm_install_title">Yükləmə Təsdiqi</string>
<!--Settings-->
<string name="settings_dark_mode_title">Mövzu Modu</string>
<string name="settings_dark_mode_message">Zövqünüzə uyğun modu seçin!</string>
<string name="settings_dark_mode_light">Həmişə Açıq</string>
<string name="settings_dark_mode_system">Sistemə Uyğun</string>
<string name="settings_dark_mode_dark">Həmişə Qaranlıq</string>
<string name="settings_download_path_title">Endirmə yolu</string>
<string name="settings_download_path_message">Fayllar %1$s\'a yerləşdiriləcək</string>
<string name="settings_hide_app_title">Magisk tətbiqini gizlət</string>
<string name="settings_hide_app_summary">Təsadüfi paket ID\'si ilə proxy yüklə və özəl tətbiq adı yaz</string>
<string name="settings_restore_app_title">Magisk tətbiqini bərpa et</string>
<string name="settings_restore_app_summary">Tətbiqi üzə çıxar və orijinal APKnı bərpa et</string>
<string name="language">Dil</string>
<string name="system_default">(Sistem Dili)</string>
<string name="settings_check_update_title">Yeniləməni Yoxla</string>
<string name="settings_check_update_summary">Vaxtaşırı arxaplanda yeniləmələri yoxla</string>
<string name="settings_update_channel_title">Yeniləmə Kanalı</string>
<string name="settings_update_stable">Sabit</string>
<string name="settings_update_beta">Beta</string>
<string name="settings_update_custom">Özəl</string>
<string name="settings_update_custom_msg">Özəl kanal URL\'si daxil et</string>
<string name="settings_zygisk_summary">Magiskin bəzi hissələrini zygote daemonda aç</string>
<string name="settings_denylist_title">İstisnaları Məcbur İşlət</string>
<string name="settings_denylist_summary">İstisnalar siyahısındakı proseslər Magisk əlavələrini tərsinə çevirəcək</string>
<string name="settings_denylist_config_title">İstisnaları Ayarla</string>
<string name="settings_denylist_config_summary">İstisnalara yerləşdiriləcək prosesi seçin</string>
<string name="settings_hosts_title">Sistemsiz hostlar</string>
<string name="settings_hosts_summary">Reklam əngəlləyici tətbiqlər üçün host faylları</string>
<string name="settings_hosts_toast">Sistemsiz hostların əlavəsi quruldu</string>
<string name="settings_app_name_hint">Yeni Ad</string>
<string name="settings_app_name_helper">Tətbiq bu adla yenidən paketlənəcək</string>
<string name="settings_app_name_error">Keçərsiz Format</string>
<string name="settings_su_app_adb">Tətbiqlər və ADB</string>
<string name="settings_su_app">Yalmız Tətbiqlər</string>
<string name="settings_su_adb">Yalnız ADB</string>
<string name="settings_su_disable">Heç Biri</string>
<string name="settings_su_request_10">10 saniyə</string>
<string name="settings_su_request_15">15 saniyə</string>
<string name="settings_su_request_20">20 saniyə</string>
<string name="settings_su_request_30">30 saniyə</string>
<string name="settings_su_request_45">45 saniyə</string>
<string name="settings_su_request_60">60 saniyə</string>
<string name="superuser_access">Super istifadəçi İcazəsi</string>
<string name="auto_response">Avtomatik Cavab</string>
<string name="request_timeout">İstək Vaxtaşımı</string>
<string name="superuser_notification">Super İstifadəçi Bildirişi</string>
<string name="settings_su_reauth_title">Yüksəltdikdən sonra yenidən səlahiyətləndir</string>
<string name="settings_su_reauth_summary">Tətbiqləri yüksəltdikdən sonra Super İstifadəçi icazəsi istə</string>
<string name="settings_su_tapjack_title">Saxta Ekran (Tapjacking) Qoruması</string>
<string name="settings_su_tapjack_summary">Super İstifadəçi icazə pəncərəsi digər pəncərələr tərəfindən əngəlləndikdə heçbir girişə cavab verməyəcək</string>
<string name="settings_customization">Özəlləşdirmə</string>
<string name="setting_add_shortcut_summary">Tətbiqi gizlətdikdən sonra tapmaq çətin olmasın deyə ana ekrana qısayol əlavə et</string>
<string name="settings_doh_title">HTTPS üzərindən DNS</string>
<string name="settings_doh_description">Bəzi ölkələrdə DNS problemlərini həll edir</string>
<string name="multiuser_mode">Çox istifadəçi Modu</string>
<string name="settings_owner_only">Yalnız Cihazın Sahibi</string>
<string name="settings_owner_manage">Cihaz Sahibinin İdarəsində</string>
<string name="settings_user_independent">İstifadəçidən Qeyri-Asılı</string>
<string name="owner_only_summary">Yalnız cihaz sahibində root icazəsi var</string>
<string name="owner_manage_summary">Yalnız cihaz sahibində root icazəsi verə yaxud istəyə bilər</string>
<string name="user_independent_summary">Hər istifadəçinin öz root icazəsi var</string>
<string name="mount_namespace_mode">Adlarfəzası modunu qoş</string>
<string name="settings_ns_global">Beynəlxalq Adlarfəzası</string>
<string name="settings_ns_requester">Adlarfəzası Daxil et</string>
<string name="settings_ns_isolate">Təklənmiş Adlarfəzası</string>
<string name="global_summary">Bütün kök girişləri beynəlxaq adlarfəzası işlədəcək</string>
<string name="requester_summary">Bütün kök girişləri öz adlarfəzalarından icazə alacaq</string>
<string name="isolate_summary">Hər kök giriş üçün ayrıca kök adlarfəzası olacaq</string>
<!--Notifications-->
<string name="update_channel">Magisk Yeniləmələri</string>
<string name="progress_channel">İrəliləmə Bildirişləri</string>
<string name="updated_channel">Yeniləmə Bitdi</string>
<string name="download_complete">Endirmə Bitdi</string>
<string name="download_file_error">Faylı endirmək münkün olmadı</string>
<string name="magisk_update_title">Magisk Yeniləməsi Var!</string>
<string name="updated_title">Magisk Yeniləndi</string>
<string name="updated_text">Tətbiqi açmaq üçün toxun</string>
<!--Toasts, Dialogs-->
<string name="yes">Mövcud</string>
<string name="no">Mövcud Deyil</string>
<string name="repo_install_title">Quraşdır %1$s %2$s(%3$d)</string>
<string name="download">Endir</string>
<string name="reboot">Yenidən başlat</string>
<string name="release_notes">Yeniliklər</string>
<string name="flashing">Flashlanır...</string>
<string name="done">Bitdi!</string>
<string name="failure">Alınmadı!</string>
<string name="hide_app_title">Magisk tətbiqi gizlədilir…</string>
<string name="open_link_failed_toast">Keçidi açaçaq tətbiq yoxdur</string>
<string name="complete_uninstall">Tamamilə sil</string>
<string name="restore_img">Nüsxələri Bərpa et</string>
<string name="restore_img_msg">Bərpa Edilir…</string>
<string name="restore_done">Bərpa Edildi!</string>
<string name="restore_fail">Bərpa üçün nüxsələr mövcud deyil!</string>
<string name="setup_fail">Qurma uğursuz oldu</string>
<string name="env_fix_title">Əlavə yükləməyə ethiyac var</string>
<string name="env_fix_msg">Cihazınız Magiskin düzgün işləməsi üçün əlavə yükləməyə ehtiyac duyur. Yenidən başlatmaq istəyirsiniz?</string>
<string name="setup_msg">Yüklənir…</string>
<string name="unsupport_magisk_title">Bu Magisk versiyası dəstəklənmir</string>
<string name="unsupport_magisk_msg">Tətbiqin bu versiyası Magiskin %1$s versiyasından aşağıları dəstəkləmir.\n\nTətbiq sanki Magisk heç yoxmuş kimi davranacaq, lütfən Magiski yüksəldin.</string>
<string name="unsupport_general_title">Anormal Hal</string>
<string name="unsupport_system_app_msg">Bu tətbiqi sistem tətbiqi kimi açmaq olmur. Onu yenidən istifadəçi tətbiqi etməlisiniz.</string>
<string name="unsupport_other_su_msg">Magiskdən olmayan \"su\" tapıldı. Lütfən digər kök tətbiqi silin yaxud Magiski yenidən quraşdırın.</string>
<string name="unsupport_external_storage_msg">Magisk xarici yaddaşa qurulub. Lütfən tətbiqi daxili yaddaşa keçirin.</string>
<string name="unsupport_nonroot_stub_msg">Gizli Magisk tətbiqi kök itirildiyinə görə işləməyəcək. Orijinal APKnı geri qaytarın.</string>
<string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string>
<string name="external_rw_permission_denied">Bu funksiyanı işə salmaq üçün yaddaş icazəsi verin</string>
<string name="post_notifications_denied">Bu funksiyanı işə salmaq üçün bildiriş icazəsi verin</string>
<string name="install_unknown_denied">"bilinməyən tətbiqləri quraşdır"\ı açsanız bu funksiya işə düşəcək</string>
<string name="add_shortcut_title">Ana ekranda qısayol yarat</string>
<string name="add_shortcut_msg">Tətbiqi gizlətdikdən sonra tapmaq çətin olmasın deyə ana ekranda qısayol yaradılsın?</string>
<string name="app_not_found">Bu işi görəcək heçbir tətbiq yoxdur</string>
<string name="reboot_apply_change">Dəyişikliklər təkrar başladıqdan sonra olacaq</string>
<string name="restore_app_confirmation">Gizli tətbiq əvvəlki halına qayıdacaq. Davam edilsin?</string>
</resources>

View file

@ -0,0 +1,250 @@
<resources>
<!--Author: Radoš Milićev (https://github.com/rammba)-->
<!--Sections-->
<string name="modules">Moduli</string>
<string name="superuser">Super-korisnik</string>
<string name="logs">Logovi</string>
<string name="settings">Podešavanja</string>
<string name="install">Instalacija</string>
<string name="section_home">Početno</string>
<string name="section_theme">Teme</string>
<string name="denylist">Lista zabrana</string>
<!--Home-->
<string name="no_connection">Nedostupna konekcija</string>
<string name="app_changelog">Promene u aplikaciji</string>
<string name="loading">Učitavanje…</string>
<string name="update">Ažuriranje</string>
<string name="not_available">N/A</string>
<string name="hide">Sakrij</string>
<string name="home_package">Paket</string>
<string name="home_app_title">Apl.</string>
<string name="home_notice_content">Preuzmite Magisk SAMO sa zvanične GitHub stranice. Fajlovi iz nepoznatih izvora mogu biti maliciozni!</string>
<string name="home_support_title">Podržite nas</string>
<string name="home_follow_title">Zapratite nas</string>
<string name="home_item_source">Izvor</string>
<string name="home_support_content">Magisk jeste i uvek će biti besplatan i open source. Međutim, možete pokazati da vam je stalo svojom donacijom.</string>
<string name="home_installed_version">Instalirano</string>
<string name="home_latest_version">Najnovije</string>
<string name="invalid_update_channel">Nevalidan kanal ažuriranja</string>
<string name="uninstall_magisk_title">Deinstaliraj Magisk</string>
<string name="uninstall_magisk_msg">Svi moduli će biti onemogućeni/uklonjeni!\nKoren će biti uklonjen!\nSvako neenkriptovano interno skladište će upotrebom Magisk-a biti ponovo enkriptovano!</string>
<!--Install-->
<string name="keep_force_encryption">Zadrži forsiranu enkripciju</string>
<string name="keep_dm_verity">Zadrži AVB 2.0/dm-verity</string>
<string name="recovery_mode">Režim oporavka</string>
<string name="install_options_title">Opcije</string>
<string name="install_method_title">Metod</string>
<string name="install_next">Naredno</string>
<string name="install_start">Počnimo</string>
<string name="manager_download_install">Pritisni da preuzmeš i instaliraš</string>
<string name="direct_install">Direktna instalacija (Preporučeno)</string>
<string name="install_inactive_slot">Instalacija na neaktivan slot (Nakon OTA)</string>
<string name="install_inactive_slot_msg">Vaš uređaj će biti FORSIRAN da se pokrene na trenutno neaktivnom slotu nakon ponovnog pokretanja!\nKoristite opciju samo kad se OTA završi.\nNastavi?</string>
<string name="setup_title">Dodatne postavke</string>
<string name="select_patch_file">Izaberite fajl</string>
<string name="patch_file_msg">Izaberite sliku (*.img) ili ODIN tarfile (*.tar) ili payload.bin (*.bin)</string>
<string name="reboot_delay_toast">Ponovo pokretanje za 5 sekundi…</string>
<string name="flash_screen_title">Instalacija</string>
<!--Superuser-->
<string name="su_request_title">Super-korisnički zahtev</string>
<string name="touch_filtered_warning">Magisk ne može da verifikuje vaš odgovor, jer aplikacija prikriva super-korisnički zahtev.</string>
<string name="deny">Zabrani</string>
<string name="prompt">Zahtev</string>
<string name="restrict">Ograniči</string>
<string name="grant">Dozvoli</string>
<string name="su_warning">Pruža potpun pristup vašem uređaju.\nZabranite ako niste sigurni!</string>
<string name="forever">Zauvek</string>
<string name="once">Jednom</string>
<string name="tenmin">10 min</string>
<string name="twentymin">20 min</string>
<string name="thirtymin">30 min</string>
<string name="sixtymin">60 min</string>
<string name="su_allow_toast">%1$s je dobio prava na super-korisnika</string>
<string name="su_deny_toast">%1$s nije dobio prava na super-korisnika</string>
<string name="su_snack_grant">Super-korisnička prava od %1$s su pružena</string>
<string name="su_snack_deny">Super-korisnička prava od %1$s su odbijena</string>
<string name="su_snack_notif_on">Notifikacije od %1$s su omogućene</string>
<string name="su_snack_notif_off">Notifikacije od %1$s su onemogućene</string>
<string name="su_snack_log_on">Logovanje za %1$s je omogućeno</string>
<string name="su_snack_log_off">Logovanje za %1$s je onemogućeno</string>
<string name="su_revoke_title">Opozovi?</string>
<string name="su_revoke_msg">Potvrdi da opozoveš prava na super-korisnika od %1$s?</string>
<string name="toast">Toast</string>
<string name="none">Ništa</string>
<string name="superuser_toggle_notification">Notifikacije</string>
<string name="superuser_toggle_revoke">Opozovi</string>
<string name="superuser_policy_none">Nijedna aplikacija nije tražila permisije za super-korisnika još uvek.</string>
<!--Logs-->
<string name="log_data_none">Nemate logova. Pokušajte koristiti korenske aplikacije više.</string>
<string name="log_data_magisk_none">Magisk logovi su prazni, to je čudno.</string>
<string name="menuSaveLog">Sačuvaj log</string>
<string name="menuClearLog">Ukloni log</string>
<string name="logs_cleared">Log uspešno uklonjen</string>
<string name="pid">PID: %1$d</string>
<string name="target_uid">Ciljani UID: %1$d</string>
<string name="target_pid">Ciljani PID: %s</string>
<string name="selinux_context">SELinux kontekst: %s</string>
<string name="supp_group">Dopunska grupa: %s</string>
<!--MagiskHide-->
<string name="show_system_app">Prikaži sistemske apl.</string>
<string name="show_os_app">Prikaži apl. OS-a</string>
<string name="hide_filter_hint">Filtriraj po imenu</string>
<string name="hide_search">Pretraga</string>
<!--Module-->
<string name="no_info_provided">(Bez informacija)</string>
<string name="reboot_userspace">Lako ponovo pokretanje</string>
<string name="reboot_recovery">Ponovo pokreni za oporavak</string>
<string name="reboot_bootloader">Ponovo pokreni za bootloader</string>
<string name="reboot_download">Ponovo pokreni za preuzimanje</string>
<string name="reboot_edl">Ponovo pokreni za EDL</string>
<string name="reboot_safe_mode">Siguran mod</string>
<string name="module_version_author">%1$s od %2$s</string>
<string name="module_state_remove">Ukloni</string>
<string name="module_action">Akcija</string>
<string name="module_state_restore">Povrati</string>
<string name="module_action_install_external">Instaliraj iz skladišta</string>
<string name="update_available">Ažuriranje dostupno</string>
<string name="suspend_text_riru">Modul je suspendovan jer je %1$s omogućeno</string>
<string name="suspend_text_zygisk">Modul je suspendovan jer %1$s nije omogućeno</string>
<string name="zygisk_module_unloaded">Zygisk modul nije učitan zbog nekompatibilnosti</string>
<string name="module_empty">Nijedan modul nije instaliran</string>
<string name="confirm_install">Instaliraj modul %1$s?</string>
<string name="confirm_install_title">Potvrda instalacije</string>
<!--Settings-->
<string name="settings_dark_mode_title">Tema</string>
<string name="settings_dark_mode_message">Izaberite temu koja vam najviše odgovara!</string>
<string name="settings_dark_mode_light">Uvek svetlo</string>
<string name="settings_dark_mode_system">Prati sistem</string>
<string name="settings_dark_mode_dark">Uvek tamno</string>
<string name="settings_download_path_title">Putanja za preuzimanje</string>
<string name="settings_download_path_message">Fajlovi će biti sačuvani na %1$s</string>
<string name="settings_hide_app_title">Sakrij Magisk apl.</string>
<string name="settings_hide_app_summary">Instaliraj proxy aplikaciju sa nasumičnim ID-jem paketa i prilagođenom labelom</string>
<string name="settings_restore_app_title">Povrati Magisk apl.</string>
<string name="settings_restore_app_summary">Otkrij apl. i povrati originalni APK</string>
<string name="language">Jezik</string>
<string name="system_default">(Podrazumevano sistemski)</string>
<string name="settings_check_update_title">Proveri ažuriranja</string>
<string name="settings_check_update_summary">Periodično proveri ažuriranja u pozadini</string>
<string name="settings_update_channel_title">Kanal ažuriranja</string>
<string name="settings_update_stable">Stabilno</string>
<string name="settings_update_beta">Beta</string>
<string name="settings_update_debug">Debug</string>
<string name="settings_update_custom">Prilagođeno</string>
<string name="settings_update_custom_msg">Unesi prilagođeni URL kanala</string>
<string name="settings_zygisk_summary">Pokreni delove Magisk-a u Zygote daemon-u</string>
<string name="settings_denylist_title">Sprovedi listu zabrana</string>
<string name="settings_denylist_summary">Procesi na listi zabrana će povratiti sve Magisk izmene</string>
<string name="settings_denylist_config_title">Konfiguriši listu zabrana</string>
<string name="settings_denylist_config_summary">Izaberi procese koji će biti na listi zabrana</string>
<string name="settings_hosts_title">Bezsistemski domaćini (hosts)</string>
<string name="settings_hosts_summary">Podrška bezsistemskih domaćina za aplikacije blokiranja reklama</string>
<string name="settings_hosts_toast">Modul bezsistemskih domaćina dodat</string>
<string name="settings_app_name_hint">Novo ime</string>
<string name="settings_app_name_helper">Apl. će biti spakovana pod ovim imenom</string>
<string name="settings_app_name_error">Nevalidan format</string>
<string name="settings_su_app_adb">Aplikacije i ADB</string>
<string name="settings_su_app">Samo aplikacije</string>
<string name="settings_su_adb">Samo ADB</string>
<string name="settings_su_disable">Onemogućeno</string>
<string name="settings_su_request_10">10 sekundi</string>
<string name="settings_su_request_15">15 sekundi</string>
<string name="settings_su_request_20">20 sekundi</string>
<string name="settings_su_request_30">30 sekundi</string>
<string name="settings_su_request_45">45 sekundi</string>
<string name="settings_su_request_60">60 sekundi</string>
<string name="superuser_access">Pristup super-korisnika</string>
<string name="auto_response">Automatski odgovor</string>
<string name="request_timeout">Istek zahteva</string>
<string name="superuser_notification">Notifikacije super-korisnika</string>
<string name="settings_su_reauth_title">Ponovo odobri nakon ažuriranja</string>
<string name="settings_su_reauth_summary">Ponovo traži permisije super-korisnika nakon ažuriranja aplikacija</string>
<string name="settings_su_tapjack_title">Zaštita od tapjacking-a</string>
<string name="settings_su_tapjack_summary">Prompt dijalog super-korisnika neće reagovati dok je prikriven drugim prozorom ili overlay-em</string>
<string name="settings_su_auth_title">Autentifikacija korisnika</string>
<string name="settings_su_auth_summary">Traži autentifikaciju korisnika tokom zahteva super-korisnika</string>
<string name="settings_su_auth_insecure">Nijedan metod autentifikacije nije podešen na uređaju</string>
<string name="settings_su_restrict_title">Ograniči korenske sposobnosti</string>
<string name="settings_su_restrict_summary">Podrazumevano ograničava apl. super-korisnika. Upozorenje: ovo će većinu aplikacija skršiti. Ne omogućavaj, osim ako znaš šta radiš.</string>
<string name="settings_customization">Prilagođavanje</string>
<string name="setting_add_shortcut_summary">Dodaj lepu prečicu na početni ekran u slučaju da se ime i ikonica ne prepoznaju lako nakon skrivanja aplikacije</string>
<string name="settings_doh_title">DNS preko HTTPS-a</string>
<string name="settings_doh_description">Zaobilazno rešenje DNS trovanja u nekim nacijama</string>
<string name="settings_random_name_title">Nasumično ime na izlazu</string>
<string name="settings_random_name_description">Nasumično ime izlaznog fajla slika i tar fajlova radi sprečavanja detekcije</string>
<string name="multiuser_mode">Višekorisnički režim</string>
<string name="settings_owner_only">Samo vlasnik uređaja</string>
<string name="settings_owner_manage">Određeno od strane vlasnika</string>
<string name="settings_user_independent">Nezavisno od korisnika</string>
<string name="owner_only_summary">Samo vlasnik ima pristup korenu</string>
<string name="owner_manage_summary">Samo vlasnik može da pristupa korenu i da prima zahteve za njega</string>
<string name="user_independent_summary">Svaki korisnik ima svoja pravila korena</string>
<string name="mount_namespace_mode">Mount režim namespace-a</string>
<string name="settings_ns_global">Globalni namespace</string>
<string name="settings_ns_requester">Nasleđeni namespace</string>
<string name="settings_ns_isolate">Izolovani namespace</string>
<string name="global_summary">Sve korenske sesije koriste globalni mount namespace</string>
<string name="requester_summary">Korenske sesije će naslediti namespace od podnosioca zahteva</string>
<string name="isolate_summary">Svaka korenska sesija će imati svoj izolovani namespace</string>
<!--Notifications-->
<string name="update_channel">Ažuriranja Magisk-a</string>
<string name="progress_channel">Notifikacije o progresu</string>
<string name="updated_channel">Ažuriranje završeno</string>
<string name="download_complete">Preuzimanje završeno</string>
<string name="download_file_error">Greška pri preuzimanju fajla</string>
<string name="magisk_update_title">Ažuriranje Magisk-a dostupno!</string>
<string name="updated_title">Magisk je ažuriran</string>
<string name="updated_text">Klikni da otvoriš aplikaciju</string>
<!--Toasts, Dialogs-->
<string name="yes">Da</string>
<string name="no">Ne</string>
<string name="repo_install_title">Instaliraj %1$s %2$s(%3$d)</string>
<string name="download">Preuzmi</string>
<string name="reboot">Ponovo pokreni</string>
<string name="close">Zatvori</string>
<string name="release_notes">Release notes</string>
<string name="flashing">Flešovanje…</string>
<string name="running">Pokretanje…</string>
<string name="done">Završeno!</string>
<string name="done_action">Pokretanje akcije %1$s završeno</string>
<string name="failure">Neuspešno!</string>
<string name="hide_app_title">Skrivanje Magisk aplikacije…</string>
<string name="open_link_failed_toast">Nije pronađena aplikacija za otvaranje linka</string>
<string name="complete_uninstall">Kompletna deinstalacija</string>
<string name="restore_img">Povrati slike</string>
<string name="restore_img_msg">Povratak…</string>
<string name="restore_done">Povratak uspešan!</string>
<string name="restore_fail">Fabrički bekap ne postoji!</string>
<string name="setup_fail">Neuspešna postavka</string>
<string name="env_fix_title">Potrebno dodatno podešavanje</string>
<string name="env_fix_msg">Vaš uređaj zahteva dodatno podešavanje da bi Magisk radio kako treba. Da li želite nastaviti i pokrenuti ponovo?</string>
<string name="env_full_fix_msg">Vaš uređaj zahteva ponovno flešovanje da bi Magisk radio kako treba. Reinstalirajte Magisk kroz aplikaciju, režim oporavka ne može dobiti tačne informacije o uređaju.</string>
<string name="setup_msg">Pokretanje podešavanja okruženja…</string>
<string name="unsupport_magisk_title">Nepodržana verzija Magisk-a</string>
<string name="unsupport_magisk_msg">Ova verzija aplikacije ne podržava Magisk verzije manje od %1$s.\n\nAplikacija će se ponašati kao da Magisk nije instaliran. Molimo ažurirajte Magisk što pre.</string>
<string name="unsupport_general_title">Nenormalno stanje</string>
<string name="unsupport_system_app_msg">Pokretanje aplikacije kao sistemske nije podržano. Molimo postavite aplikaciju da bude korisnička.</string>
<string name="unsupport_other_su_msg">Detektovan \"su\" binary koji nije Magisk-ov. Molimo uklonite konkurentno korensko rešenje i/ili reinstalirajte Magisk.</string>
<string name="unsupport_external_storage_msg">Magisk je instaliran na eksterno skladište. Molimo pomerite apl. u interno skladište.</string>
<string name="unsupport_nonroot_stub_msg">Skrivena Magisk aplikacija ne može nastaviti sa radom jer je koren izgubljen. Molimo povratite originalni APK.</string>
<string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string>
<string name="external_rw_permission_denied">Dozvolite permisiju za skladište da biste omogućili ovu funkcionalnost</string>
<string name="post_notifications_denied">Dozvolite permisiju za notifikacije da biste omogućili ovu funkcionalnost</string>
<string name="install_unknown_denied">Dozvolite \"instaliranje nepoznatih aplikacija\" da biste omogućili ovu funkcionalnost</string>
<string name="add_shortcut_title">Dodaj prečicu na početni ekran</string>
<string name="add_shortcut_msg">Nakon skrivanja aplikacije, njeno ime i ikonicu ćete teško prepoznati. Želite li dodati lepu prečicu na početni ekran?</string>
<string name="app_not_found">Nije pronađena aplikacija za ovu akciju</string>
<string name="reboot_apply_change">Ponovo pokreni da primeniš izmene</string>
<string name="restore_app_confirmation">Ovo će vratiti skrivenu aplikaciju na originalnu. Da li stvarno to želite?</string>
</resources>

View file

@ -0,0 +1,202 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Sections-->
<string name="modules">Модулі</string>
<string name="superuser">Правы суперкарыстальніка</string>
<string name="logs">Журналы</string>
<string name="settings">Налады</string>
<string name="install">Усталёўка</string>
<string name="section_home">Хатняя старонка</string>
<string name="section_theme">Тэмы</string>
<!--Home-->
<string name="no_connection">Злучэнне адсутнічае</string>
<string name="app_changelog">Спіс змен</string>
<string name="loading">Загрузка…</string>
<string name="update">Абнавіць</string>
<string name="not_available">Не</string>
<string name="hide">Схаваць</string>
<string name="home_package">Пакунак</string>
<string name="home_app_title">Праграма</string>
<string name="home_notice_content">Спампоўваце Magisk ТОЛЬКІ з афіцыйнай старонкі на GitHub. Файлы з невядомых крыніц могуць быць шкоднаснымі!</string>
<string name="home_support_title">Падтрымайце нас</string>
<string name="home_item_source">Зыходны код</string>
<string name="home_support_content">Magisk ёсць і заўсёды будзе бясплатным праектам з адкрытым зыходным кодам. Але вы заўсёды можаце ахвяраваць нам на распрацоўку.</string>
<string name="home_installed_version">Усталявана</string>
<string name="home_latest_version">Апошні</string>
<string name="invalid_update_channel">Хібны канал абнаўлення</string>
<string name="uninstall_magisk_title">Выдаліць Magisk</string>
<string name="uninstall_magisk_msg">Усе модулі будуць адключаныя/выдаленыя!\nRoot будзе выдалены!\nВашыя даныя будуць зашыфраваныя!</string>
<!--Install-->
<string name="keep_force_encryption">Прымусова захаваць шыфраванне</string>
<string name="keep_dm_verity">Захаваць AVB 2.0/dm-verity</string>
<string name="recovery_mode">Рэжым Recovery</string>
<string name="install_options_title">Параметры</string>
<string name="install_method_title">Метад</string>
<string name="install_next">Далей</string>
<string name="install_start">Усталяваць</string>
<string name="manager_download_install">Націсніце, каб спампаваць і ўсталяваць</string>
<string name="direct_install">Непасрэдная ўсталёўка (рэкамендуецца)</string>
<string name="install_inactive_slot">Усталяваць у неактыўны слот (пасля OTA)</string>
<string name="install_inactive_slot_msg">Ваша прылада ПРЫМУСОВА перазапусціцца ў неактыўны слот!\nВыкарыстоўвайце гэты параметр толькі пасля завяршэння OTA.\nПрацягнуць?</string>
<string name="setup_title">Дадатковая ўсталёўка</string>
<string name="select_patch_file">Абраць файл і ўжыць да яго патч</string>
<string name="patch_file_msg">Абярыце файл вобраза (*.img) альбо архіў ODIN (*.tar)</string>
<string name="reboot_delay_toast">Перазапуск праз 5 секунд…</string>
<string name="flash_screen_title">Усталёўка</string>
<!--Superuser-->
<string name="su_request_title">Запыт правоў суперкарыстальніка</string>
<string name="touch_filtered_warning">Праграма перакрывае запыт на выдачу правоў суперкарыстальніка, таму Magisk не можа апрацаваць ваш адказ</string>
<string name="deny">Адмовіць</string>
<string name="prompt">Запытаць</string>
<string name="grant">Даць</string>
<string name="su_warning">Даць поўны доступ да вашай прылады.\nКалі вы не ўпэўненыя, адхіліце запыт!</string>
<string name="forever">Назаўсёды</string>
<string name="once">Адзін раз</string>
<string name="tenmin">10 хвіл</string>
<string name="twentymin">20 хвіл</string>
<string name="thirtymin">30 хвіл</string>
<string name="sixtymin">60 хвіл</string>
<string name="su_allow_toast">Праграме \"%1$s\" дадзеныя правы суперкарыстальніка</string>
<string name="su_deny_toast">Праграме \"%1$s\" было адмоўлена ў правах суперкарыстальніка</string>
<string name="su_snack_grant">Праграме \"%1$s\" дадзеныя правы суперкарыстальніка</string>
<string name="su_snack_deny">Праграме \"%1$s\" адмоўлена ў правах суперкарыстальніка</string>
<string name="su_snack_notif_on">Апавяшчэнні для \"%1$s\" уключаныя</string>
<string name="su_snack_notif_off">Апавяшчэнні для \"%1$s\" адключаныя</string>
<string name="su_snack_log_on">Вядзенне журнала для \"%1$s\" уключана</string>
<string name="su_snack_log_off">Вядзенне журнала для \"%1$s\" адключана</string>
<string name="su_revoke_title">Адклікаць?</string>
<string name="su_revoke_msg">Адклікаць правы для \"%1$s\"?</string>
<string name="toast">Выплыўныя апавяшчэнні</string>
<string name="none">Нічога</string>
<string name="superuser_toggle_notification">Апавяшчэнні</string>
<string name="superuser_toggle_revoke">Адклікаць</string>
<string name="superuser_policy_none">Праграма яшчэ не запытвала правоў суперкарыстальніка.</string>
<!--Logs-->
<string name="log_data_none">Журналаў няма. Паспрабуйце даць праграмам правы суперкарыстальніка.</string>
<string name="log_data_magisk_none">Журналы адсутнічаюць.</string>
<string name="menuSaveLog">Захаваць журнал</string>
<string name="menuClearLog">Ачысціць журнал</string>
<string name="logs_cleared">Журнал паспяхова ачышчаны</string>
<string name="pid">PID: %1$d</string>
<string name="target_uid">Мэтавы UID: %1$d</string>
<!--SafetyNet-->
<!--MagiskHide-->
<string name="show_system_app">Паказваць сістэмныя праграмы</string>
<string name="show_os_app">Паказваць сістэмныя праграмы</string>
<string name="hide_filter_hint">Фільтраваць па назве</string>
<string name="hide_search">Пошук</string>
<!--Module-->
<string name="no_info_provided">(Няма інфармацыі)</string>
<string name="reboot_userspace">Праграмны перазапуск</string>
<string name="reboot_recovery">Перазапуск у Recovery</string>
<string name="reboot_bootloader">Перазапуск у Bootloader</string>
<string name="reboot_download">Перазапуск у Download</string>
<string name="reboot_edl">Перазапуск у EDL</string>
<string name="module_version_author">%1$s ад %2$s</string>
<string name="module_state_remove">Выдаліць</string>
<string name="module_state_restore">Аднавіць</string>
<string name="module_action_install_external">Усталяваць са сховішча</string>
<string name="update_available">Даступныя абнаўленні</string>
<!--Settings-->
<string name="settings_dark_mode_title">Рэжым афармлення</string>
<string name="settings_dark_mode_message">Абярыце рэжым, які вам больш даспадобы!</string>
<string name="settings_dark_mode_light">Заўсёды светлы</string>
<string name="settings_dark_mode_system">Сістэмны</string>
<string name="settings_dark_mode_dark">Заўсёды цёмны</string>
<string name="settings_download_path_title">Каталог спамповак</string>
<string name="settings_download_path_message">Файлы будуць спампоўвацца ў %1$s</string>
<string name="settings_hide_app_title">Схаваць праграму Magisk</string>
<string name="settings_hide_app_summary">Усталяваць проксі-праграму з выпадковым ідэнтыфікатарам пакунка і адвольным значком</string>
<string name="settings_restore_app_title">Аднавіць праграму Magisk</string>
<string name="settings_restore_app_summary">Вярнуць праграму да зыходнага стану</string>
<string name="language">Мова</string>
<string name="system_default">(Сістэмная)</string>
<string name="settings_check_update_title">Правяраць на абнаўленні</string>
<string name="settings_check_update_summary">Перыядычна правяраць наяўнасць абнаўленняў ў фонавым рэжыме</string>
<string name="settings_update_channel_title">Канал абнаўлення</string>
<string name="settings_update_stable">Стабільны</string>
<string name="settings_update_beta">Бэта</string>
<string name="settings_update_custom">Адвольны канал</string>
<string name="settings_update_custom_msg">Устаўце URL</string>
<string name="settings_hosts_title">Пазасістэмны файл hosts</string>
<string name="settings_hosts_summary">Падтрымка пазасістэмнага файла hosts для праграм, якія блакуюць рэкламу</string>
<string name="settings_hosts_toast">Дададзены модуль пазасістэмнага файла hosts</string>
<string name="settings_app_name_hint">Новая назва</string>
<string name="settings_app_name_helper">Праграма будзе перапакаваная з гэтай назвай</string>
<string name="settings_app_name_error">Хібны фармат</string>
<string name="settings_su_app_adb">Праграмы і ADB</string>
<string name="settings_su_app">Толькі праграмы</string>
<string name="settings_su_adb">Толькі ADB</string>
<string name="settings_su_disable">Адключана</string>
<string name="settings_su_request_10">10 секунд</string>
<string name="settings_su_request_15">15 секунд</string>
<string name="settings_su_request_20">20 секунд</string>
<string name="settings_su_request_30">30 секунд</string>
<string name="settings_su_request_45">45 секунд</string>
<string name="settings_su_request_60">60 секунд</string>
<string name="superuser_access">Доступ суперкарыстальніка</string>
<string name="auto_response">Аўтаматычны адказ</string>
<string name="request_timeout">Чаканне адказу</string>
<string name="superuser_notification">Апавяшчэнне суперкарыстальніка</string>
<string name="settings_su_reauth_title">Паўторная аўтэнтыфікацыя пасля абнаўлення</string>
<string name="settings_su_reauth_summary">Паўторна запытваць правы суперкарыстальніка пасля абнаўлення праграмы</string>
<string name="settings_su_tapjack_title">Уключыць абарону ад перахоплівання ўводу</string>
<string name="settings_su_tapjack_summary">Дыялог выдачы правоў суперкарыстальніка не будзе адказваць на ўвод, калі па-над ім знаходзяцца іншыя вокны</string>
<string name="settings_customization">Персаналізацыя</string>
<string name="setting_add_shortcut_summary">Дадаць на хатні экран прыгожы цэтлік на той выпадак, калі пасля хавання праграмы будзе цяжка разглядзець значок і назву</string>
<string name="settings_doh_title">DNS паверх HTTPS</string>
<string name="settings_doh_description">Абыходны шлях для DNS у некаторых краінах</string>
<string name="multiuser_mode">Шматкарыстальніцкі рэжым</string>
<string name="settings_owner_only">Толькі ўладальнік</string>
<string name="settings_owner_manage">Кіраванне ўладальнікам</string>
<string name="settings_user_independent">Правілы карыстальнікаў</string>
<string name="owner_only_summary">Толькі ўладальніку даступны рэжым суперкарыстальніка</string>
<string name="owner_manage_summary">Толькі ўладальнік кіруе доступам суперкарыстальніка і апрацоўвае запыты</string>
<string name="user_independent_summary">Кожны карыстальнік кіруе правіламі доступу суперкарыстальніка</string>
<string name="mount_namespace_mode">Наладка прастораў назваў</string>
<string name="settings_ns_global">Агульная прастора назваў</string>
<string name="settings_ns_requester">Спадкаемная прастора назваў</string>
<string name="settings_ns_isolate">Ізаляваная прастора назваў</string>
<string name="global_summary">Сеансы суперкарыстальніка выкарыстоўваюць агульную прастору назваў</string>
<string name="requester_summary">Сеансы суперкарыстальніка выкарыстоўваюць спадкаемную прастору назваў</string>
<string name="isolate_summary">Сеансы суперкарыстальніка выкарыстоўваюць ізаляваную прастору назваў</string>
<!--Notifications-->
<string name="update_channel">Абнаўленні Magisk</string>
<string name="progress_channel">Апавяшчэнні пра ход выканання</string>
<string name="download_complete">Спампоўка завершаная</string>
<string name="download_file_error">Не атрымалася спампаваць файл</string>
<string name="magisk_update_title">Даступна абнаўленне Magisk!</string>
<!--Toasts, Dialogs-->
<string name="yes">Так</string>
<string name="no">Не</string>
<string name="repo_install_title">Усталяваць %1$s %2$s(%3$d)</string>
<string name="download">Спампаваць</string>
<string name="reboot">Перазапуск</string>
<string name="release_notes">Пра выпуск</string>
<string name="flashing">Усталёўка…</string>
<string name="done">Завершана!</string>
<string name="failure">Не атрымалася</string>
<string name="hide_app_title">Хаванне праграмы Magisk…</string>
<string name="open_link_failed_toast">Праграмы для адкрыцця спасылкі не знойдзена</string>
<string name="complete_uninstall">Поўнае выдаленне</string>
<string name="restore_img">Аднавіць раздзелы</string>
<string name="restore_img_msg">Аднаўленне…</string>
<string name="restore_done">Аднаўленне завершана!</string>
<string name="restore_fail">Рэзервовая копія адсутнічае!</string>
<string name="setup_fail">Усталяваць не атрымалася</string>
<string name="env_fix_title">Патрабуецца дадатковая ўсталёўка</string>
<string name="env_fix_msg">Для карэктнай працы на вашай прыладзе спатрэбіцца ўсталяваць дадатковыя кампаненты. Хочаце працягнуць?</string>
<string name="setup_msg">Наладка асяроддзя…</string>
<string name="unsupport_magisk_title">Непадтрымліваемая версія Magisk</string>
<string name="unsupport_magisk_msg">Гэтая версія праграмы не падтрымлівае Magisk версіі ніжэй за %1$s.\n\nПраграма будзе працаваць так, як быццам Magisk не ўсталяваны. Як мага хутчэй абнавіце Magisk.</string>
<string name="unsupport_general_title">Анамальны стан</string>
<string name="unsupport_system_app_msg">Гэтую праграму немагчыма запусціць як сістэмную. Калі ласка, вярніце праграму да стану карыстальніцкай.</string>
<string name="unsupport_other_su_msg">Выяўлены загад \"su\", які не належыць Magisk. Калі ласка, выдаліце іншы кліент.</string>
<string name="unsupport_external_storage_msg">Праграма Magisk усталяваная ў вонкавым сховішчы. Калі ласка, перамясціце яе ва ўнітарнае сховішча.</string>
<string name="unsupport_nonroot_stub_msg">Праграма не можа працягваць працу ў схаваным стане, бо root-доступ быў страчаны. Вярніце праграму да зыходнага стану.</string>
<string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string>
<string name="external_rw_permission_denied">Дайце доступ да сховішча, каб уключыць гэтую функцыю</string>
<string name="add_shortcut_title">Дадаць цэтлік на хатні экран</string>
<string name="add_shortcut_msg">Пасля хавання праграмы значок і назву можа быць цяжка знайсці. Хочаце дадаць цэтлік на хатні экран?</string>
<string name="app_not_found">Не знойдзена праграм для апрацоўкі гэтага дзеяння</string>
</resources>

View file

@ -0,0 +1,238 @@
<resources>
<!--Sections-->
<string name="modules">Модули</string>
<string name="superuser">Superuser</string>
<string name="logs">Дневник</string>
<string name="settings">Настройки</string>
<string name="install">Инсталиране</string>
<string name="section_home">Начало</string>
<string name="section_theme">Теми</string>
<string name="denylist">Черен списък</string>
<!--Home-->
<string name="no_connection">Няма връзка</string>
<string name="app_changelog">Списък с промени</string>
<string name="loading">Зареждане…</string>
<string name="update">Обновяване</string>
<string name="not_available">Липсва</string>
<string name="hide">Скриване</string>
<string name="home_package">Пакет</string>
<string name="home_app_title">Приложение</string>
<string name="home_notice_content">Изтегляйте Magisk САМО от официалната страница в GitHub. Файловете от неизвестни източници могат да бъдат зловредни!</string>
<string name="home_support_title">Подкрепете ни</string>
<string name="home_follow_title">Последвайте ни</string>
<string name="home_item_source">Изходен код</string>
<string name="home_support_content">Magisk е, и винаги ще бъде, безплатен и с отворен изходен код. Въпреки това можете да покажете подкрапата си чрез дарение.</string>
<string name="home_installed_version">Инсталиранo</string>
<string name="home_latest_version">Последно</string>
<string name="invalid_update_channel">Каналът за обновяване е недействителен</string>
<string name="uninstall_magisk_title">Премахване на Magisk</string>
<string name="uninstall_magisk_msg">Всички модули ще бъдат изключени/премахнати. Достъпът до правата на суперпотребителя ще бъде премахнат!\nАко вътрешното хранилище е разшифровано с Magisk, то ще бъде шифровано отново!</string>
<!--Install-->
<string name="keep_force_encryption">Запазване на наложеното шифроване</string>
<string name="keep_dm_verity">Запазване на AVB 2.0/dm-verity</string>
<string name="recovery_mode">Режим за възстановяване</string>
<string name="install_options_title">Настройки</string>
<string name="install_method_title">Метод</string>
<string name="install_next">Напред</string>
<string name="install_start">Начало</string>
<string name="manager_download_install">За да изтеглите и инсталирате, докоснете</string>
<string name="direct_install">Директно инсталиране (препоръчително)</string>
<string name="install_inactive_slot">Инсталиране в неактивен дял (след OTA)</string>
<string name="install_inactive_slot_msg">Устройство ЗАДЪЛЖИТЕЛНО ще зареди от текущия неактивен дял при следващия рестарт.\nИзползвайте само при приключила инсталация на OTA.\nПродължаване?</string>
<string name="setup_title">Допълнителни настройки</string>
<string name="select_patch_file">Избор и закърпване на файл</string>
<string name="patch_file_msg">Изберете образ (*.img) или архив на ODIN (*.tar)</string>
<string name="reboot_delay_toast">Рестартиране след 5 секунди…</string>
<string name="flash_screen_title">Инсталиране</string>
<!--Superuser-->
<string name="su_request_title">Запитване за достъп</string>
<string name="touch_filtered_warning">Тъй като друго приложение закрива заявката за достъп до правата на суперпотребителя, Magisk не може да потвърди вашия отговор</string>
<string name="deny">Отказ</string>
<string name="prompt">Запитване</string>
<string name="grant">Разрешаване</string>
<string name="su_warning">Дава пълен достъп до устройството.\nОткажете, ако не сте сигурни.</string>
<string name="forever">Винаги</string>
<string name="once">Веднъж</string>
<string name="tenmin">10 мин.</string>
<string name="twentymin">20 мин.</string>
<string name="thirtymin">30 мин.</string>
<string name="sixtymin">60 мин.</string>
<string name="su_allow_toast">%1$s получи достъп до суперпотребителя</string>
<string name="su_deny_toast">%1$s не получи достъп до суперпотребителя</string>
<string name="su_snack_grant">Достъп до суперпотребителя е разрешен на %1$s</string>
<string name="su_snack_deny">Достъп до суперпотребителя е отказан на %1$s</string>
<string name="su_snack_notif_on">Известията за %1$s са включени</string>
<string name="su_snack_notif_off">Известията за %1$s са изключени</string>
<string name="su_snack_log_on">Записването в дневника за %1$s е включено</string>
<string name="su_snack_log_off">Записването в дневника за %1$s е изключено</string>
<string name="su_revoke_title">Оттегляне?</string>
<string name="su_revoke_msg">Потвърждавате ли оттегляне на достъпа на %1$s?</string>
<string name="toast">Тост</string>
<string name="none">Без</string>
<string name="superuser_toggle_notification">Известия</string>
<string name="superuser_toggle_revoke">Оттегляне</string>
<string name="superuser_policy_none">За момента няма приложения, които да са поискали достъп до правата на суперпотребителя.</string>
<!--Logs-->
<string name="log_data_none">Дневникът е празен</string>
<string name="log_data_magisk_none">Дневникът на Magisk е празен</string>
<string name="menuSaveLog">Запазване на дневника</string>
<string name="menuClearLog">Изчистване на дневника</string>
<string name="logs_cleared">Дневникът е изчистен</string>
<string name="pid">PID: %1$d</string>
<string name="target_uid">Целеви UID: %1$d</string>
<!--SafetyNet-->
<!--MagiskHide-->
<string name="show_system_app">Показване на системни приложения</string>
<string name="show_os_app">Показване на приложения на ОС</string>
<string name="hide_filter_hint">Филтрира по име</string>
<string name="hide_search">Търсене</string>
<!--Module-->
<string name="no_info_provided">(Липсва информация)</string>
<string name="reboot_userspace">Бърз рестарт</string>
<string name="reboot_recovery">Рестарт в режим за възстановяване</string>
<string name="reboot_bootloader">Рестарт в bootloader</string>
<string name="reboot_download">Рестарт в режим за изтегляне</string>
<string name="reboot_edl">Рестарт в EDL</string>
<string name="module_version_author">%1$s от %2$s</string>
<string name="module_state_remove">Премахване</string>
<string name="module_state_restore">Възстановяване</string>
<string name="module_action_install_external">Инсталиране от хранилището</string>
<string name="update_available">Има обновяване</string>
<string name="suspend_text_riru">Модулът е спрян, защото е включено: %1$s</string>
<string name="suspend_text_zygisk">Модулът е спрян, защото не е включено: %1$s</string>
<string name="zygisk_module_unloaded">Поради несъвместимост модулът Zygisk не е зареден</string>
<string name="module_empty">Не са инсталирани модули</string>
<string name="confirm_install">Инсталиране на модула %1$s?</string>
<string name="confirm_install_title">Подвърждаване на инсталиране</string>
<!--Settings-->
<string name="settings_dark_mode_title">Режим на темата</string>
<string name="settings_dark_mode_message">Изберете режима, който ви отива най-много!</string>
<string name="settings_dark_mode_light">Винаги светло</string>
<string name="settings_dark_mode_system">Според системата</string>
<string name="settings_dark_mode_dark">Винаги тъмно</string>
<string name="settings_download_path_title">Папка за изтегляния</string>
<string name="settings_download_path_message">Файловете ще бъдат запазвани в %1$s</string>
<string name="settings_hide_app_title">Скриване на приложението на Magisk</string>
<string name="settings_hide_app_summary">Инсталира междинно приложение с произволен идестификатор и име</string>
<string name="settings_restore_app_title">Възстановяване на приложението на Magisk</string>
<string name="settings_restore_app_summary">Показва и възстановява оригиналното приложение</string>
<string name="language">Език</string>
<string name="system_default">(Според системата)</string>
<string name="settings_check_update_title">Проверка за обновяване</string>
<string name="settings_check_update_summary">Периодична проверка за обновяване във фонов режим</string>
<string name="settings_update_channel_title">Канал за обновяване</string>
<string name="settings_update_stable">Стабилен</string>
<string name="settings_update_beta">Бета</string>
<string name="settings_update_custom">Потребителски</string>
<string name="settings_update_custom_msg">Адрес на потребителски канал</string>
<string name="settings_zygisk_summary">Изпълняване на части от Magisk в демон на zygote</string>
<string name="settings_denylist_title">Налагане на черен списък</string>
<string name="settings_denylist_summary">На процесите в черния списък ще бъдат възстановени всички модификации, направени от Magisk</string>
<string name="settings_denylist_config_title">Настройка на черен списък</string>
<string name="settings_denylist_config_summary">Изберете процесите, които да бъдат включени в черния списък</string>
<string name="settings_hosts_title">Безсистемни хостове</string>
<string name="settings_hosts_summary">Поддръжка на безсистемни хостове, ползвани от приложения за спиране на реклами</string>
<string name="settings_hosts_toast">Модулът за безсистемни хостове е добавен</string>
<string name="settings_app_name_hint">Ново име</string>
<string name="settings_app_name_helper">Приложението ще бъде пакетирано с това име</string>
<string name="settings_app_name_error">Форматът е недействителен</string>
<string name="settings_su_app_adb">Приложения и ADB</string>
<string name="settings_su_app">Само приложения</string>
<string name="settings_su_adb">Само ADB</string>
<string name="settings_su_disable">Изключен</string>
<string name="settings_su_request_10">10 секунди</string>
<string name="settings_su_request_15">15 секунди</string>
<string name="settings_su_request_20">20 секунди</string>
<string name="settings_su_request_30">30 секунди</string>
<string name="settings_su_request_45">45 секунди</string>
<string name="settings_su_request_60">60 секунди</string>
<string name="superuser_access">Достъп до суперпотребителя</string>
<string name="auto_response">Автоматичен отговор</string>
<string name="request_timeout">Време за запитване</string>
<string name="superuser_notification">Известие за суперпотребителя</string>
<string name="settings_su_reauth_title">Повторно запитване след обновяване</string>
<string name="settings_su_reauth_summary">Повторно запитване за достъп до правата на суперпотребителя след обновяване на приложения</string>
<string name="settings_su_tapjack_title">Предпазване от измамно докосване</string>
<string name="settings_su_tapjack_summary">Прозорецът за достъп до суперпотребителя няма да реагира, докато е покрит от друг прозорец или слой</string>
<string name="settings_customization">Приспособяване</string>
<string name="setting_add_shortcut_summary">Добавя красив пряк път на началния екран ако името или текущата икона на скритото приложение са трудни за разпознаване</string>
<string name="settings_doh_title">DNS през HTTPS</string>
<string name="settings_doh_description">Заобикаля „отравянето“ на DNS в някои държави</string>
<string name="multiuser_mode">Многопотребителски режим</string>
<string name="settings_owner_only">Само собственика</string>
<string name="settings_owner_manage">Управление от страна на собственика</string>
<string name="settings_user_independent">Независимо от потребителя</string>
<string name="owner_only_summary">Само собственикът има достъп до суперпотребителя</string>
<string name="owner_manage_summary">Само собственикът може да управлява достъпа до суперпотребителя и да получава запитвания за достъп</string>
<string name="user_independent_summary">Всеки потребител има собствени правила за достъп до суперпотребителя</string>
<string name="mount_namespace_mode">Монтиране по пространства от имена</string>
<string name="settings_ns_global">Общо</string>
<string name="settings_ns_requester">Наследено</string>
<string name="settings_ns_isolate">Изолирано</string>
<string name="global_summary">Всички сесии с достъп до суперпотребителя използват общото пространство от имена</string>
<string name="requester_summary">Всички сесии с достъп до суперпотребителя наследяват пространството от имена на запитващото приложение</string>
<string name="isolate_summary">Всички сесии с достъп до суперпотребителя получават изолирани пространства от имена</string>
<!--Notifications-->
<string name="update_channel">Обновяване на Magisk</string>
<string name="progress_channel">Известия за напредък</string>
<string name="updated_channel">Завършено обновяване</string>
<string name="download_complete">Завършено изтегляне</string>
<string name="download_file_error">Грешка при изтегляне на файл</string>
<string name="magisk_update_title">Има обновяване на Magisk!</string>
<string name="updated_title">Magisk е обновен</string>
<string name="updated_text">За да отворите приложението, докоснете</string>
<!--Toasts, Dialogs-->
<string name="yes">Да</string>
<string name="no">Не</string>
<string name="repo_install_title">Инсталиране на %1$s %2$s(%3$d)</string>
<string name="download">Изтегляне</string>
<string name="reboot">Рестартиране</string>
<string name="release_notes">Бележки по изданието</string>
<string name="flashing">Инсталиране…</string>
<string name="done">Готово!</string>
<string name="failure">Грешка!</string>
<string name="hide_app_title">Скриване на приложението на Magisk…</string>
<string name="open_link_failed_toast">Не е намерено приложение, с което препратката да бъде отворена</string>
<string name="complete_uninstall">Премахване</string>
<string name="restore_img">Възстановяване на образи</string>
<string name="restore_img_msg">Възстановяване…</string>
<string name="restore_done">Възстановяването е успешно!</string>
<string name="restore_fail">На устройството липсва резервно копие на заводския образ!</string>
<string name="setup_fail">Грешка при първоначална настройка</string>
<string name="env_fix_title">Необходима е допълнителна настройка</string>
<string name="env_fix_msg">За да работи Magisk нормално, устройството се нуждае от допълнителна настройка. Да бъде ли продължено след рестарт?</string>
<string name="env_full_fix_msg">За да работи Magisk нормално, е необходимо да бъде инсталиран отново. Преинсталирайте Magisk от приложението, защото режимът за възстановяване не получава необходимата информация за устройството.</string>
<string name="setup_msg">Надстройка на средата…</string>
<string name="unsupport_magisk_title">Неподдържано издание на Magisk</string>
<string name="unsupport_magisk_msg">Това издание на приложението не поддържа издания на Magisk преди %1$s.\n\nПриложението ще се държи все едно няма инсталиран Magisk. Обновете Magisk възможно най-скоро.</string>
<string name="unsupport_general_title">Неочаквано състояние</string>
<string name="unsupport_system_app_msg">Приложението не се поддържа да работи като системно. Върнете го като потребителско.</string>
<string name="unsupport_other_su_msg">Намерен е двоичен файл „su“, който не е от Magisk. Премахнете всички други решения за достъп до правата на суперпотребителя и/или инсталирайте Magisk отново.</string>
<string name="unsupport_external_storage_msg">Magisk е инсталиран във външно хранилище. Преместете приложението във вътрешното хранилище.</string>
<string name="unsupport_nonroot_stub_msg">Скритото приложение на Magisk не може да продължи да работи, защото достъпът до правата на суперпотребителя е загубен. Възстановете оригиналното приложение.</string>
<string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string>
<string name="external_rw_permission_denied">За да използвате нази възможност, разрешете достъп до хранилището</string>
<string name="post_notifications_denied">За да използвате нази възможност, разрешете достъп до известията</string>
<string name="install_unknown_denied">За да използвате тази възможност, разрешете „инсталиране на неизвестни приложения“</string>
<string name="add_shortcut_title">Добавяне на пряк път на началния екран</string>
<string name="add_shortcut_msg">След скриване на приложението името или икона може да станат трудни за разпознаване. Желаете ли да бъде добаве красив пряк път на началния екран?</string>
<string name="app_not_found">Не е намерено приложение, което да извърши действието</string>
<string name="reboot_apply_change">Рестартиране за прилагане на промените</string>
<string name="restore_app_confirmation">По този начин ще възстановите скритото приложение в първоначалното му състояние. Това ли желаете да бъде направено?</string>
</resources>

View file

@ -0,0 +1,234 @@
<resources>
<!--Sections-->
<string name="modules">মডিউল</string>
<string name="superuser">সুপার ইউজার</string>
<string name="logs">লগ</string>
<string name="settings">সেটিংস</string>
<string name="install">ইনস্টল করুন</string>
<string name="section_home">হোম</string>
<string name="section_theme">থিম</string>
<string name="denylist">ডিনাইলিস্ট</string>
<!--Home-->
<string name="no_connection">কোনো সংযোগ উপলব্ধ নেই</string>
<string name="app_changelog">চেঞ্জলগ</string>
<string name="loading">লোড হচ্ছে…</string>
<string name="update">হালনাগাদ</string>
<string name="not_available">N/A</string>
<string name="hide">লুকান</string>
<string name="home_package">প্যাকেজ</string>
<string name="home_app_title">অ্যাপ</string>
<string name="home_notice_content">শুধুমাত্র অফিসিয়াল গিটহাব পেজ থেকে ম্যাজিস্ক ডাউনলোড করুন। অজানা উৎস থেকে ফাইল ক্ষতিকর হতে পারে!</string>
<string name="home_support_title">আমাদের সমর্থন</string>
<string name="home_follow_title">আমাদের অনুসরণ করো</string>
<string name="home_item_source">সূত্র</string>
<string name="home_support_content">ম্যাজিস্ক হল, এবং সবসময় থাকবে, বিনামূল্যে, এবং ওপেন সোর্স। তবে আপনি দান করার মাধ্যমে আমাদের দেখাতে পারেন যে আপনি যত্নশীল।</string>
<string name="home_installed_version">ইনস্টল করা হয়েছে</string>
<string name="home_latest_version">সর্বশেষ</string>
<string name="invalid_update_channel">অবৈধ আপডেট চ্যানেল</string>
<string name="uninstall_magisk_title">ম্যাজিস্ক আনইনস্টল করুন</string>
<string name="uninstall_magisk_msg">সমস্ত মডিউল নিষ্ক্রিয়/মুছে ফেলা হবে!\nরুট সরানো হবে!\nম্যাজিস্ক ব্যবহারের মাধ্যমে এনক্রিপ্ট করা যে কোনও অভ্যন্তরীণ স্টোরেজ পুনরায় এনক্রিপ্ট করা হবে!</string>
<!--Install-->
<string name="keep_force_encryption">এনক্রিপশন সংরক্ষণ করুন</string>
<string name="keep_dm_verity">AVB 2.0/dm-verity সংরক্ষণ করুন</string>
<string name="recovery_mode">পুনরুদ্ধার অবস্থা</string>
<string name="install_options_title">অপশন</string>
<string name="install_method_title">পদ্ধতি</string>
<string name="install_next">পরবর্তী</string>
<string name="install_start">চলো যাই</string>
<string name="manager_download_install">ডাউনলোড এবং ইনস্টল করুন</string>
<string name="direct_install">সরাসরি ইনস্টল</string>
<string name="install_inactive_slot">নিষ্ক্রিয় স্লটে ইনস্টল করুন (OTA এর পরে)</string>
<string name="install_inactive_slot_msg">রিবুট করার পরে আপনার ডিভাইসটিকে বর্তমান নিষ্ক্রিয় স্লটে বুট করতে বাধ্য করা হবে!\It হয়ে গেলেই এই বিকল্পটি ব্যবহার করুন।\চালিয়ে রাখবেন?</string>
<string name="setup_title">অতিরিক্ত সেটআপ</string>
<string name="select_patch_file">একটি ফাইল নির্বাচন করুন এবং প্যাচ করুন</string>
<string name="patch_file_msg">একটি কাঁচা চিত্র (*.img) বা একটি ODIN টারফাইল (*.tar) নির্বাচন করুন</string>
<string name="reboot_delay_toast">৫ সেকেন্ডের মধ্যে রিবুট হচ্ছে...</string>
<string name="flash_screen_title">স্থাপন</string>
<!--Superuser-->
<string name="su_request_title">সুপার ইউজার অনুরোধ/স্ট্রিং</string>
<string name="touch_filtered_warning">যেহেতু একটি অ্যাপ সুপার ব্যবহারকারীর অনুরোধকে অস্পষ্ট করছে, ম্যাজিস্ক আপনার প্রতিক্রিয়া যাচাই করতে পারে না</string>
<string name="deny">অস্বীকার করুন</string>
<string name="prompt">শীঘ্র</string>
<string name="grant">প্রদান</string>
<string name="su_warning">আপনার ডিভাইসে সম্পূর্ণ অ্যাক্সেস মঞ্জুর করে৷\nআপনি নিশ্চিত না হলে অস্বীকার করুন!</string>
<string name="forever">চিরতরে</string>
<string name="once">একদা</string>
<string name="tenmin">১০ মিনিট</string>
<string name="twentymin">২০ মিনিট</string>
<string name="thirtymin">৩০ মিনিট</string>
<string name="sixtymin">৬০ মিনিট</string>
<string name="su_allow_toast">%1$s সুপার ইউজার অধিকার দেওয়া হয়েছিল</string>
<string name="su_deny_toast">%1$s সুপার ইউজার অধিকার অস্বীকার করা হয়েছিল</string>
<string name="su_snack_grant">%1$s-এর সুপার ব্যবহারকারীর অধিকার মঞ্জুর করা হয়েছে</string>
<string name="su_snack_deny">%1$s-এর সুপার ব্যবহারকারীর অধিকার অস্বীকার করা হয়েছে৷</string>
<string name="su_snack_notif_on">%1$s-এর বিজ্ঞপ্তিগুলি সক্ষম করা হয়েছে৷</string>
<string name="su_snack_notif_off">%1$s-এর বিজ্ঞপ্তিগুলি অক্ষম করা হয়েছে৷</string>
<string name="su_snack_log_on">%1$s এর লগিং সক্ষম করা হয়েছে৷</string>
<string name="su_snack_log_off">%1$s এর লগিং অক্ষম করা হয়েছে৷</string>
<string name="su_revoke_title">প্রত্যাহার করুন?</string>
<string name="su_revoke_msg">%1$s সুপার ব্যবহারকারীর অধিকার প্রত্যাহার করতে নিশ্চিত করুন৷</string>
<string name="toast">টোস্ট</string>
<string name="none">কোনোটিই নয়</string>
<string name="superuser_toggle_notification">বিজ্ঞপ্তি</string>
<string name="superuser_toggle_revoke">প্রত্যাহার করুন</string>
<string name="superuser_policy_none">কোনো অ্যাপ এখনও সুপার ইউজারের অনুমতি চায়নি।</string>
<!--Logs-->
<string name="log_data_none">আপনি লগ-মুক্ত, আপনার রুট অ্যাপগুলি আরও ব্যবহার করার চেষ্টা করুন৷</string>
<string name="log_data_magisk_none">ম্যাজিস্ক লগগুলি খালি, এটি অদ্ভুত</string>
<string name="menuSaveLog">লগ সংরক্ষণ</string>
<string name="menuClearLog">এখন লগ সাফ করুন</string>
<string name="logs_cleared">লগ সফলভাবে সাফ করা হয়েছে৷</string>
<string name="pid">PID: %1$d</string>
<string name="target_uid">লক্ষ্য UID: %1$d</string>
<!--SafetyNet-->
<!--MagiskHide-->
<string name="show_system_app">সিস্টেম অ্যাপ দেখান</string>
<string name="show_os_app">ওএস অ্যাপ দেখান</string>
<string name="hide_filter_hint">নাম অনুসারে ফিল্টার করুন</string>
<string name="hide_search">অনুসন্ধান করুন</string>
<!--Module-->
<string name="no_info_provided">(কোন তথ্য প্রদান করা হয়</string>
<string name="reboot_userspace">নরম রিবুট</string>
<string name="reboot_recovery">রিকভারিতে রিবুট করুন</string>
<string name="reboot_bootloader">বুটলোডারে রিবুট করুন</string>
<string name="reboot_download">ডাউনলোড করতে রিবুট করুন</string>
<string name="reboot_edl">EDL এ রিবুট করুন</string>
<string name="module_version_author">%1$s by %2$s</string>
<string name="module_state_remove">অপসারণ</string>
<string name="module_state_restore">পুনরুদ্ধার করুন</string>
<string name="module_action_install_external">স্টোরেজ থেকে ইনস্টল করুন</string>
<string name="update_available">আপডেট উপলব্ধ</string>
<string name="suspend_text_riru">মডিউল সাসপেন্ড করা হয়েছে কারণ %1$s সক্ষম হয়েছে৷</string>
<string name="suspend_text_zygisk">মডিউল সাসপেন্ড করা হয়েছে কারণ %1$s সক্ষম করা নেই৷</string>
<string name="zygisk_module_unloaded">অসামঞ্জস্যতার কারণে জাইগিস্ক মডিউল লোড করা হয়নি</string>
<string name="module_empty">কোন মডিউল ইনস্টল করা নেই</string>
<!--Settings-->
<string name="settings_dark_mode_title">থিম মোড</string>
<string name="settings_dark_mode_message">আপনার শৈলী সবচেয়ে উপযুক্ত মোড নির্বাচন করুন!</string>
<string name="settings_dark_mode_light">সবসময় আলো</string>
<string name="settings_dark_mode_system">সিস্টেম অনুসরণ করুন</string>
<string name="settings_dark_mode_dark">সবসময় অন্ধকার</string>
<string name="settings_download_path_title">পাথ ডাউনলোড করুন</string>
<string name="settings_download_path_message">ফাইলগুলি %1$s এ সংরক্ষণ করা হবে৷</string>
<string name="settings_hide_app_title">ম্যাজিস্ক অ্যাপটি লুকান</string>
<string name="settings_hide_app_summary">একটি র্যান্ডম প্যাকেজ আইডি এবং কাস্টম অ্যাপ লেব সহ একটি প্রক্সি অ্যাপ ইনস্টল করুনl</string>
<string name="settings_restore_app_title">ম্যাজিস্ক অ্যাপটি পুনরুদ্ধার করুন</string>
<string name="settings_restore_app_summary">অ্যাপটি আড়াল করুন এবং আসলটি পুনরুদ্ধার করুন</string>
<string name="language">ভাষা</string>
<string name="system_default">(সিস্টেমের ডিফল্ট)</string>
<string name="settings_check_update_title">আপডেট চেক করুন</string>
<string name="settings_check_update_summary">পর্যায়ক্রমে পটভূমিতে আপডেটের জন্য চেক করুন</string>
<string name="settings_update_channel_title">চ্যানেল আপডেট করুন</string>
<string name="settings_update_stable">স্থিতিশীল</string>
<string name="settings_update_beta">বেটা</string>
<string name="settings_update_custom">কাস্টম</string>
<string name="settings_update_custom_msg">একটি কাস্টম চ্যানেল URL সন্নিবেশ করুন</string>
<string name="settings_zygisk_summary">জাইগোট ডেমনে ম্যাজিস্কের অংশগুলি চালান</string>
<string name="settings_denylist_title">তালিকা অস্বীকার করুন প্রয়োগ করুন</string>
<string name="settings_denylist_summary">ডিনালিস্টের প্রসেসগুলিতে সমস্ত ম্যাজিস্ক পরিবর্তনগুলি ফিরিয়ে দেওয়া হবে</string>
<string name="settings_denylist_config_title">অস্বীকার তালিকা কনফিগার করুন</string>
<string name="settings_denylist_config_summary">অস্বীকৃত তালিকায় অন্তর্ভুক্ত করার জন্য প্রক্রিয়াগুলি নির্বাচন করুন৷</string>
<string name="settings_hosts_title">সিস্টেমহীন হোস্ট</string>
<string name="settings_hosts_summary">বিজ্ঞাপন ব্লকিং অ্যাপের জন্য সিস্টেমলেস হোস্ট</string>
<string name="settings_hosts_toast">সিস্টেমহীন হোস্ট মডিউল যোগ করা হয়েছে</string>
<string name="settings_app_name_hint">নতুন নাম</string>
<string name="settings_app_name_helper">অ্যাপটি এই নামের সাথে পুনরায় প্যাকেজ করা হবে</string>
<string name="settings_app_name_error">ভুল ফরম্যাট</string>
<string name="settings_su_app_adb">অ্যাপস এবং এডিবি</string>
<string name="settings_su_app">শুধুমাত্র অ্যাপস</string>
<string name="settings_su_adb">শুধুমাত্র এডিবি</string>
<string name="settings_su_disable">অক্ষম</string>
<string name="settings_su_request_10">১০ সেকেন্ড</string>
<string name="settings_su_request_15">১৫ সেকেন্ড</string>
<string name="settings_su_request_20">২০ সেকেন্ড</string>
<string name="settings_su_request_30">৩০ সেকেন্ড</string>
<string name="settings_su_request_45">৪৫ সেকেন্ড</string>
<string name="settings_su_request_60">৬০ সেকেন্ড</string>
<string name="superuser_access">সুপার ইউজার অ্যাক্সেস</string>
<string name="auto_response">স্বয়ংক্রিয় প্রতিক্রিয়া</string>
<string name="request_timeout">অনুরোধের সময়সীমা শেষ</string>
<string name="superuser_notification">সুপার ইউজার বিজ্ঞপ্তি</string>
<string name="settings_su_reauth_title">আপগ্রেড করার পরে পুনরায় প্রমাণীকরণ করুন</string>
<string name="settings_su_reauth_summary">অ্যাপগুলি আপগ্রেড করার পরে আবার সুপার ইউজার অনুমতির জন্য জিজ্ঞাসা করুন</string>
<string name="settings_su_tapjack_title">ট্যাপজ্যাকিং সুরক্ষা</string>
<string name="settings_su_tapjack_summary">সুপার ইউজার প্রম্পট ডায়ালগ অন্য কোনো উইন্ডো বা ওভারলে দ্বারা অস্পষ্ট থাকাকালীন ইনপুটটিতে সাড়া দেবে না</string>
<string name="settings_customization">কাস্টমাইজেশন</string>
<string name="setting_add_shortcut_summary">অ্যাপটি লুকানোর পরে নাম এবং আইকন সনাক্ত করা কঠিন হলে হোম স্ক্রিনে একটি সুন্দর শর্টকাট যোগ করুন</string>
<string name="settings_doh_title">HTTPS এর উপর ডিএনএস</string>
<string name="settings_doh_description">কিছু দেশে ডিএনএস বিষক্রিয়ার সমাধান</string>
<string name="multiuser_mode">মাল্টিউজার মোড</string>
<string name="settings_owner_only">শুধুমাত্র ডিভাইসের মালিক</string>
<string name="settings_owner_manage">ডিভাইস মালিক পরিচালিত</string>
<string name="settings_user_independent">ব্যবহারকারী-স্বাধীন</string>
<string name="owner_only_summary">শুধুমাত্র মালিকের রুট অ্যাক্সেস আছে</string>
<string name="owner_manage_summary">শুধুমাত্র মালিকই রুট অ্যাক্সেস পরিচালনা করতে পারে এবং অনুরোধ প্রম্পট গ্রহণ করতে পারে</string>
<string name="user_independent_summary">প্রতিটি ব্যবহারকারীর নিজস্ব পৃথক রুট নিয়ম আছে</string>
<string name="mount_namespace_mode">মাউন্ট নেমস্পেস মোড</string>
<string name="settings_ns_global">গ্লোবাল নেমস্পেস</string>
<string name="settings_ns_requester">নেমস্পেস ইনহেরিট করুন</string>
<string name="settings_ns_isolate">বিচ্ছিন্ন নামস্থান</string>
<string name="global_summary">সমস্ত রুট সেশন গ্লোবাল মাউন্ট নেমস্পেস ব্যবহার করে</string>
<string name="requester_summary">রুট সেশন অনুরোধকারীর নামস্থান উত্তরাধিকারী হবে</string>
<string name="isolate_summary">প্রতিটি রুট সেশনের নিজস্ব বিচ্ছিন্ন নামস্থান থাকবে</string>
<!--Notifications-->
<string name="update_channel">ম্যাজিস্ক আপডেট</string>
<string name="progress_channel">অগ্রগতি বিজ্ঞপ্তি</string>
<string name="updated_channel">আপডেট সম্পূর্ণ</string>
<string name="download_complete">ডাউনলোড শেষ</string>
<string name="download_file_error">ফাইল ডাউনলোড করার সময় ত্রুটি</string>
<string name="magisk_update_title">ম্যাজিস্ক আপডেট উপলব্ধ!</string>
<string name="updated_title">ম্যাজিস্ক আপডেট</string>
<string name="updated_text">অ্যাপ খুলতে আলতো চাপুন</string>
<!--Toasts, Dialogs-->
<string name="yes">হ্যাঁ</string>
<string name="no">না</string>
<string name="repo_install_title">ইনস্টল করুন %1$s %2$s(%3$d)</string>
<string name="download">ডাউনলোড করুন</string>
<string name="reboot">রিবুট করুন</string>
<string name="release_notes">অব্যাহতি পত্র</string>
<string name="flashing">ঝলকানি…</string>
<string name="done">সম্পন্ন!</string>
<string name="failure">ব্যর্থ হয়েছে!</string>
<string name="hide_app_title">ম্যাজিস্ক অ্যাপটি লুকিয়ে রাখছে…</string>
<string name="open_link_failed_toast">লিঙ্ক খোলার জন্য কোনো অ্যাপ পাওয়া যায়নি</string>
<string name="complete_uninstall">সম্পূর্ণ আনইনস্টল করুন</string>
<string name="restore_img">ছবি পুনরুদ্ধার করুন</string>
<string name="restore_img_msg">পুনরুদ্ধার করা হচ্ছে…</string>
<string name="restore_done">পুনরুদ্ধার করা হয়েছে!</string>
<string name="restore_fail">স্টক ব্যাকআপ বিদ্যমান নেই!</string>
<string name="setup_fail">সেটআপ ব্যর্থ হয়েছে৷</string>
<string name="env_fix_title">অতিরিক্ত সেটআপ প্রয়োজন</string>
<string name="env_fix_msg">ম্যাজিস্ক সঠিকভাবে কাজ করার জন্য আপনার ডিভাইসের অতিরিক্ত সেটআপ প্রয়োজন। আপনি কি এগিয়ে যেতে এবং রিবুট করতে চান?</string>
<string name="setup_msg">চলমান পরিবেশ সেটআপ…</string>
<string name="unsupport_magisk_title">অসমর্থিত ম্যাজিস্ক সংস্করণ</string>
<string name="unsupport_magisk_msg">অ্যাপটির এই সংস্করণটি %1$s-এর চেয়ে কম ম্যাগিস্ক সংস্করণগুলিকে সমর্থন করে না৷\n\nঅ্যাপটি এমন আচরণ করবে যেন কোনও ম্যাজিস্ক ইনস্টল করা নেই, দয়া করে যত তাড়াতাড়ি সম্ভব ম্যাজিস্ক আপগ্রেড করুন।</string>
<string name="unsupport_general_title">অস্বাভাবিক অবস্থা</string>
<string name="unsupport_system_app_msg">এই অ্যাপটিকে একটি সিস্টেম অ্যাপ হিসেবে চালানো সমর্থিত নয়। অনুগ্রহ করে অ্যাপটিকে একটি ব্যবহারকারী অ্যাপে ফিরিয়ে দিন।</string>
<string name="unsupport_other_su_msg">ম্যাজিস্ক থেকে নয় একটি \"su\" বাইনারি সনাক্ত করা হয়েছে। অনুগ্রহ করে কোনো প্রতিযোগী রুট সমাধান সরান এবং/অথবা ম্যাজিস্ক পুনরায় ইনস্টল করুন।</string>
<string name="unsupport_external_storage_msg">ম্যাজিস্ক বহিরাগত স্টোরেজ ইনস্টল করা হয়। অনুগ্রহ করে অ্যাপটিকে অভ্যন্তরীণ সঞ্চয়স্থানে সরান৷</string>
<string name="unsupport_nonroot_stub_msg">লুকানো ম্যাজিস্ক অ্যাপটি কাজ চালিয়ে যেতে পারে না কারণ রুট হারিয়ে গেছে। অনুগ্রহ করে আসল APK পুনরুদ্ধার করুন।</string>
<string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string>
<string name="external_rw_permission_denied">এই কার্যকারিতা সক্ষম করতে স্টোরেজ অনুমতি দিন</string>
<string name="post_notifications_denied">নোটিফিকেশন এর অনোমতি দিন</string>
<string name="install_unknown_denied">এই কার্যকারিতা সক্ষম করতে "অজানা অ্যাপগুলি ইনস্টল করুন" এর অনুমতি দিন</string>
<string name="add_shortcut_title">হোম স্ক্রিনে শর্টকাট যোগ করুন</string>
<string name="add_shortcut_msg">এই অ্যাপটি লুকানোর পরে, এর নাম এবং আইকন চিনতে অসুবিধা হতে পারে। আপনি কি হোম স্ক্রিনে একটি সুন্দর শর্টকাট যোগ করতে চান?</string>
<string name="app_not_found">এই ক্রিয়াটি পরিচালনা করার জন্য কোনো অ্যাপ পাওয়া যায়নি</string>
<string name="reboot_apply_change">পরিবর্তনগুলি প্রয়োগ করতে রিবুট করুন</string>
<string name="restore_app_confirmation">এটি লুকানো অ্যাপটিকে মূল অ্যাপে ফিরিয়ে আনবে। আপনি কি সত্যিই এটি করতে চান?</string>
</resources>

View file

@ -0,0 +1,211 @@
<resources>
<!--Sections-->
<string name="modules">Mòduls</string>
<string name="superuser">Superusuari</string>
<string name="logs">Registre</string>
<string name="settings">Configuració</string>
<string name="install">Instal·lar</string>
<string name="section_home">Inici</string>
<string name="section_theme">Temes</string>
<!--Home-->
<string name="no_connection">Connexió no disponible</string>
<string name="app_changelog">Registre de canvis</string>
<string name="loading">Carregant…</string>
<string name="update">Actualització</string>
<string name="not_available">N/A</string>
<string name="hide">Amagar</string>
<string name="home_package">Paquet</string>
<string name="home_app_title">App</string>
<string name="home_notice_content">Descarregui Magisk NOMÉS des de la pàgina oficial de GitHub. Fitxers d\'altres fonts desconegudes poden ser maliciosos!</string>
<string name="home_support_title">Doni suport</string>
<string name="home_item_source">Codi font</string>
<string name="home_support_content">Magisk és, i sempre serà, gratis i codi lliure. De totes maneres, pot mostrar el seu interès fent una petita donació.</string>
<string name="home_installed_version">Instal·lat</string>
<string name="home_latest_version">Última</string>
<string name="invalid_update_channel">Canal d\'actualització invàlid</string>
<string name="uninstall_magisk_title">Desinstal·lar Magisk</string>
<string name="uninstall_magisk_msg">Tots els mòduls seran desactivats i eliminats! L\'accés d\'arrel s\'eliminarà i, possiblement, xifrarà totes les dades (si no estan ja xifrades).</string>
<!--Install-->
<string name="keep_force_encryption">Mantenir el xifrat forçat</string>
<string name="keep_dm_verity">Mantenir AVB 2.0/dm-verity</string>
<string name="recovery_mode">Mode de Recuperació</string>
<string name="install_options_title">Opcions</string>
<string name="install_method_title">Mètode</string>
<string name="install_next">Següent</string>
<string name="install_start">Endavant</string>
<string name="manager_download_install">Premi per baixar i instal·lar</string>
<string name="direct_install">Instal·lació directa (Recomanat)</string>
<string name="install_inactive_slot">Instal·la a la ranura inactiva (Després d\'una OTA)</string>
<string name="install_inactive_slot_msg">El teu dispositiu serà FORÇAT a arrancar en l\'actual ranura inactiva després del reinici!\nUtilitza aquesta opció NOMÉS quan l\'OTA s\'hagi fet.\nContinuar?</string>
<string name="setup_title">Instal·lació addicional</string>
<string name="select_patch_file">Selecciona i arranja un arxiu</string>
<string name="patch_file_msg">Selecciona una imatge crua (*.img) o un ODIN tarfile (*.tar)</string>
<string name="reboot_delay_toast">Reinici en 5 segons…</string>
<string name="flash_screen_title">Instal·lació</string>
<!--Superuser-->
<string name="su_request_title">Petició de superusuari</string>
<string name="touch_filtered_warning">Com que una aplicació està ofuscant la petició de superusuari, Magisk no pot verificar la seva resposta</string>
<string name="deny">Denegar</string>
<string name="prompt">Preguntar</string>
<string name="grant">Permetre</string>
<string name="su_warning">Permet accés total al seu dispositiu.\nDenegui si no n\'està segur!</string>
<string name="forever">Sempre</string>
<string name="once">Un cop </string>
<string name="tenmin">10 mins</string>
<string name="twentymin">20 mins</string>
<string name="thirtymin">30 mins</string>
<string name="sixtymin">60 mins</string>
<string name="su_allow_toast">Permesos els drets de superusuari de %1$s</string>
<string name="su_deny_toast">Denegats els drets de superusuari de %1$s</string>
<string name="su_snack_grant">Drets de superusuari de %1$s permesos</string>
<string name="su_snack_deny">Drets de superusuari de %1$s denegats</string>
<string name="su_snack_notif_on">Notificacions de %1$s habilitades</string>
<string name="su_snack_notif_off">Notificacions de %1$s deshabilitades</string>
<string name="su_snack_log_on">Registres de %1$s habilitats</string>
<string name="su_snack_log_off">Registres de %1$s deshabilitats</string>
<string name="su_revoke_title">Revocar?</string>
<string name="su_revoke_msg">Confirmi per revocar drets de %1$s</string>
<string name="toast">Avís</string>
<string name="none">Cap</string>
<string name="superuser_toggle_notification">Notificacions</string>
<string name="superuser_toggle_revoke">Revocar</string>
<string name="superuser_policy_none">Cap aplicació ha demanat permisos de superusuari.</string>
<!--Logs-->
<string name="log_data_none">No hi ha cap registre. Provi d\'utilitzar aplicacions que requereixen permisos de superusuari.</string>
<string name="log_data_magisk_none">Els registres de Magisk estan buits. Això és estrany.</string>
<string name="menuSaveLog">Desar registre</string>
<string name="menuClearLog">Netejar registre ara</string>
<string name="logs_cleared">Registre netejat correctament.</string>
<string name="pid">PID: %1$d</string>
<string name="target_uid">UID de l\'objectiu: %1$d</string>
<!--SafetyNet-->
<!-- MagiskHide -->
<string name="show_system_app">Mostra aplicacions del sistema</string>
<string name="show_os_app">Mostra aplicacions del SO</string>
<string name="hide_filter_hint">Filtra per nom</string>
<string name="hide_search">Cerca</string>
<!--Module-->
<string name="no_info_provided">(No hi ha informació)</string>
<string name="reboot_userspace">Reinici suau</string>
<string name="reboot_recovery">Reiniciar en Mode Recuperació</string>
<string name="reboot_bootloader">Reiniciar en Mode Bootloader</string>
<string name="reboot_download">Reiniciar en Mode Download</string>
<string name="reboot_edl">Reiniciar a EDL</string>
<string name="module_version_author">%1$s per %2$s</string>
<string name="module_state_remove">Eliminar</string>
<string name="module_state_restore">Recuperar</string>
<string name="module_action_install_external">Instal·lar des de l\'emmagatzematge</string>
<string name="update_available">Actualització Disponible</string>
<!--Settings-->
<string name="settings_dark_mode_title">Mode del tema</string>
<string name="settings_dark_mode_message">Seleccioni el mode que més s\'adeqüi al seu estil!</string>
<string name="settings_dark_mode_light">Sempre clar</string>
<string name="settings_dark_mode_system">Seguir al sistema</string>
<string name="settings_dark_mode_dark">Sempre fosc</string>
<string name="settings_download_path_title">Directori de baixades</string>
<string name="settings_download_path_message">Els arxius es desaran a %1$s</string>
<string name="settings_hide_app_title">Amagar Magisk Manager</string>
<string name="settings_hide_app_summary">Torna a empaquetar Magisk Manager amb un nom de paquet a l\'atzar</string>
<string name="settings_restore_app_title">Restaurar Magisk Manager</string>
<string name="settings_restore_app_summary">Restaura Magisk Manager amb el nom de paquet original</string>
<string name="language">Idioma</string>
<string name="system_default">(Idioma del sistema)</string>
<string name="settings_check_update_title">Comprovar Actualitzacions</string>
<string name="settings_check_update_summary">Comprovar periòdicament en segon pla si existeixen actualitzacions</string>
<string name="settings_update_channel_title">Canal d\'Actualitzacions</string>
<string name="settings_update_stable">Estable</string>
<string name="settings_update_beta">Beta</string>
<string name="settings_update_custom">Canal personalitzat</string>
<string name="settings_update_custom_msg">Inserta un URL personalitzada</string>
<string name="settings_hosts_title">Systemless Hosts</string>
<string name="settings_hosts_summary">Suport per aplicacions tipus Adblock fora de la partició del sistema</string>
<string name="settings_hosts_toast">Agregat el mòdul Systemless Hosts</string>
<string name="settings_app_name_hint">Nou nom</string>
<string name="settings_app_name_helper">Es refarà l\'aplicació amb aquest nom</string>
<string name="settings_app_name_error">Format invàlid</string>
<string name="settings_su_app_adb">Aplicacions i ADB</string>
<string name="settings_su_app">Només aplicacions</string>
<string name="settings_su_adb">Només ADB</string>
<string name="settings_su_disable">Deshabilitat</string>
<string name="settings_su_request_10">10 segons</string>
<string name="settings_su_request_15">15 segons</string>
<string name="settings_su_request_20">20 segons</string>
<string name="settings_su_request_30">30 segons</string>
<string name="settings_su_request_45">45 segons</string>
<string name="settings_su_request_60">60 segons</string>
<string name="superuser_access">Accés de superusuari</string>
<string name="auto_response">Resposta automàtica</string>
<string name="request_timeout">Temps de petició</string>
<string name="superuser_notification">Notificació de superusuari</string>
<string name="settings_su_reauth_title">Demanar després d\'una actualització</string>
<string name="settings_su_reauth_summary">Demanar permisos de superusuari novament si una aplicació és actualitzada o reinstal·lada</string>
<string name="settings_su_tapjack_title">Activa la protecció contra \'TapJacking\'</string>
<string name="settings_su_tapjack_summary">El diàleg per donar permisos de superusuari no respondrà mentre estigui ofuscat per alguna altra finestra o superposició</string>
<string name="settings_customization">Personalització</string>
<string name="setting_add_shortcut_summary">Afegeix una bonica drecera a la pantalla d\'inici en cas que el nom i la icona siguin difícils de reconèixer després d\'amagar l\'aplicació.</string>
<string name="settings_doh_title">DNS sobre HTTPS</string>
<string name="settings_doh_description">Solució per enverinament de DNS en algunes nacions</string>
<string name="multiuser_mode">Mode Multiusuari</string>
<string name="settings_owner_only">Només Administrador del Dispositiu</string>
<string name="settings_owner_manage">Administrador del Dispositiu</string>
<string name="settings_user_independent">Usuari Independent</string>
<string name="owner_only_summary">Només l\'administrador té accés d\'arrel</string>
<string name="owner_manage_summary">Només l\'administrador pot supervisar l\'accés d\'arrel i rebre sol·licituds d\'altres usuaris</string>
<string name="user_independent_summary">Tots els usuaris tenen separades les seves pròpies regles d\'arrel</string>
<string name="mount_namespace_mode">Muntar Namespace </string>
<string name="settings_ns_global">Namespace Global</string>
<string name="settings_ns_requester">Heretar Namespace</string>
<string name="settings_ns_isolate">Aïllar Namespace</string>
<string name="global_summary">Totes les sessions d\'arrel utilitzen el suport Namespace Global</string>
<string name="requester_summary">Les sessions d\'arrel heretaran les peticions Namespace</string>
<string name="isolate_summary">Totes les sessions d\'arrel tindran la seva pròpia Namespace</string>
<!--Notifications-->
<string name="update_channel">Actualització de Magisk</string>
<string name="progress_channel">Notificacions de progrés</string>
<string name="download_complete">Baixada completada</string>
<string name="download_file_error">Error en baixar l\'arxiu</string>
<string name="magisk_update_title">Actualització de Magisk disponible!</string>
<!--Toasts, Dialogs-->
<string name="yes"></string>
<string name="no">No</string>
<string name="repo_install_title">Instal·lar %1$s %2$s(%3$d)</string>
<string name="download">Baixar</string>
<string name="reboot">Reiniciar</string>
<string name="release_notes">Notes de llançament</string>
<string name="flashing">Arranjament…</string>
<string name="done">Fet!</string>
<string name="failure">Fallit</string>
<string name="hide_app_title">Amagant Magisk Manager…</string>
<string name="open_link_failed_toast">No s\'ha trobat una aplicació per obrir l\'enllaç</string>
<string name="complete_uninstall">Desinstal·lació completa</string>
<string name="restore_img">Restaura imatges</string>
<string name="restore_img_msg">Restaurant…</string>
<string name="restore_done">Restauració feta!</string>
<string name="restore_fail">La còpia de seguretat de Estock no existeix!</string>
<string name="setup_fail">Instal·lació fallida</string>
<string name="env_fix_title">Es requereix instal·lació addicional</string>
<string name="env_fix_msg">El teu dispositiu necessita instal·lació addicional per Magisk per funcionar correctament. Es baixarà el ZIP d\'instal·lació de Magisk, vol procedir a la instal·lació ara?</string>
<string name="setup_msg">S\'està executant la configuració de l\'entorn…</string>
<string name="unsupport_magisk_title">Versió de Magisk incompatible</string>
<string name="unsupport_magisk_msg">Aquesta versió de Magisk Manager no suporta versions de Magisk més petites que la %1$s.\n\nL\'aplicació es comportarà com si Magisk no estigués instal·lat, si us plau actualitzi Magisk com més aviat millor.</string>
<string name="external_rw_permission_denied">Ha de donar permís d\'emmagatzematge per activar aquesta funcionalitat</string>
<string name="add_shortcut_title">Afegeix una drecera a la pantalla d\'inici</string>
<string name="add_shortcut_msg">Després d\'amagar Magisk Manager, el seu nom i icona poden ser difícils de reconèixer. Vols afegir una bonica drecera a la teva pantalla d\'inici?</string>
<string name="app_not_found">No s\'ha trobat una aplicació per emprar aquesta acció</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more