Repo created
This commit is contained in:
parent
d327c31227
commit
0b2aca0925
638 changed files with 76461 additions and 0 deletions
3
app/core/.gitignore
vendored
Normal file
3
app/core/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/build
|
||||
src/debug
|
||||
src/release
|
||||
72
app/core/build.gradle.kts
Normal file
72
app/core/build.gradle.kts
Normal 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
41
app/core/proguard-rules.pro
vendored
Normal 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.**
|
||||
72
app/core/src/main/AndroidManifest.xml
Normal file
72
app/core/src/main/AndroidManifest.xml
Normal 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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
27
app/core/src/main/java/com/topjohnwu/magisk/core/App.kt
Normal file
27
app/core/src/main/java/com/topjohnwu/magisk/core/App.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
131
app/core/src/main/java/com/topjohnwu/magisk/core/AppContext.kt
Normal file
131
app/core/src/main/java/com/topjohnwu/magisk/core/AppContext.kt
Normal 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) {}
|
||||
}
|
||||
209
app/core/src/main/java/com/topjohnwu/magisk/core/Config.kt
Normal file
209
app/core/src/main/java/com/topjohnwu/magisk/core/Config.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
70
app/core/src/main/java/com/topjohnwu/magisk/core/Const.kt
Normal file
70
app/core/src/main/java/com/topjohnwu/magisk/core/Const.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
55
app/core/src/main/java/com/topjohnwu/magisk/core/Hacks.kt
Normal file
55
app/core/src/main/java/com/topjohnwu/magisk/core/Hacks.kt
Normal 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,
|
||||
)
|
||||
125
app/core/src/main/java/com/topjohnwu/magisk/core/Info.kt
Normal file
125
app/core/src/main/java/com/topjohnwu/magisk/core/Info.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
103
app/core/src/main/java/com/topjohnwu/magisk/core/JobService.kt
Normal file
103
app/core/src/main/java/com/topjohnwu/magisk/core/JobService.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
app/core/src/main/java/com/topjohnwu/magisk/core/Provider.kt
Normal file
18
app/core/src/main/java/com/topjohnwu/magisk/core/Provider.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
68
app/core/src/main/java/com/topjohnwu/magisk/core/Receiver.kt
Normal file
68
app/core/src/main/java/com/topjohnwu/magisk/core/Receiver.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
app/core/src/main/java/com/topjohnwu/magisk/core/Service.kt
Normal file
39
app/core/src/main/java/com/topjohnwu/magisk/core/Service.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
149
app/core/src/main/java/com/topjohnwu/magisk/core/ktx/XAndroid.kt
Normal file
149
app/core/src/main/java/com/topjohnwu/magisk/core/ktx/XAndroid.kt
Normal 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() }
|
||||
}
|
||||
98
app/core/src/main/java/com/topjohnwu/magisk/core/ktx/XJVM.kt
Normal file
98
app/core/src/main/java/com/topjohnwu/magisk/core/ktx/XJVM.kt
Normal 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())
|
||||
}
|
||||
16
app/core/src/main/java/com/topjohnwu/magisk/core/ktx/XSU.kt
Normal file
16
app/core/src/main/java/com/topjohnwu/magisk/core/ktx/XSU.kt
Normal 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() }
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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("\\", "_")
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
124
app/core/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt
Normal file
124
app/core/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
27
app/core/src/main/java/com/topjohnwu/magisk/test/BaseTest.kt
Normal file
27
app/core/src/main/java/com/topjohnwu/magisk/test/BaseTest.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
288
app/core/src/main/java/com/topjohnwu/magisk/test/Environment.kt
Normal file
288
app/core/src/main/java/com/topjohnwu/magisk/test/Environment.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
6
app/core/src/main/res/drawable-v26/ic_launcher.xml
Normal file
6
app/core/src/main/res/drawable-v26/ic_launcher.xml
Normal 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>
|
||||
14
app/core/src/main/res/drawable-v26/sc_extension.xml
Normal file
14
app/core/src/main/res/drawable-v26/sc_extension.xml
Normal 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>
|
||||
14
app/core/src/main/res/drawable-v26/sc_superuser.xml
Normal file
14
app/core/src/main/res/drawable-v26/sc_superuser.xml
Normal 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>
|
||||
9
app/core/src/main/res/drawable/ic_extension.xml
Normal file
9
app/core/src/main/res/drawable/ic_extension.xml
Normal 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>
|
||||
5
app/core/src/main/res/drawable/ic_favorite.xml
Normal file
5
app/core/src/main/res/drawable/ic_favorite.xml
Normal 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>
|
||||
9
app/core/src/main/res/drawable/ic_fingerprint.xml
Normal file
9
app/core/src/main/res/drawable/ic_fingerprint.xml
Normal 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>
|
||||
10
app/core/src/main/res/drawable/ic_github.xml
Normal file
10
app/core/src/main/res/drawable/ic_github.xml
Normal 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>
|
||||
11
app/core/src/main/res/drawable/ic_logo.xml
Normal file
11
app/core/src/main/res/drawable/ic_logo.xml
Normal 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>
|
||||
18
app/core/src/main/res/drawable/ic_magisk.xml
Normal file
18
app/core/src/main/res/drawable/ic_magisk.xml
Normal 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>
|
||||
18
app/core/src/main/res/drawable/ic_magisk_outline.xml
Normal file
18
app/core/src/main/res/drawable/ic_magisk_outline.xml
Normal 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>
|
||||
18
app/core/src/main/res/drawable/ic_magisk_padded.xml
Normal file
18
app/core/src/main/res/drawable/ic_magisk_padded.xml
Normal 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>
|
||||
9
app/core/src/main/res/drawable/ic_more.xml
Normal file
9
app/core/src/main/res/drawable/ic_more.xml
Normal 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>
|
||||
9
app/core/src/main/res/drawable/ic_patreon.xml
Normal file
9
app/core/src/main/res/drawable/ic_patreon.xml
Normal 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>
|
||||
9
app/core/src/main/res/drawable/ic_paypal.xml
Normal file
9
app/core/src/main/res/drawable/ic_paypal.xml
Normal 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>
|
||||
9
app/core/src/main/res/drawable/ic_superuser.xml
Normal file
9
app/core/src/main/res/drawable/ic_superuser.xml
Normal 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>
|
||||
9
app/core/src/main/res/drawable/ic_twitter.xml
Normal file
9
app/core/src/main/res/drawable/ic_twitter.xml
Normal 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>
|
||||
13
app/core/src/main/res/drawable/sc_extension.xml
Normal file
13
app/core/src/main/res/drawable/sc_extension.xml
Normal 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>
|
||||
13
app/core/src/main/res/drawable/sc_superuser.xml
Normal file
13
app/core/src/main/res/drawable/sc_superuser.xml
Normal 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>
|
||||
190
app/core/src/main/res/values-ar/strings.xml
Normal file
190
app/core/src/main/res/values-ar/strings.xml
Normal 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>
|
||||
236
app/core/src/main/res/values-ast/strings.xml
Normal file
236
app/core/src/main/res/values-ast/strings.xml
Normal 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">Sí</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>
|
||||
239
app/core/src/main/res/values-az/strings.xml
Normal file
239
app/core/src/main/res/values-az/strings.xml
Normal 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>
|
||||
250
app/core/src/main/res/values-b+sr+Latn/strings.xml
Normal file
250
app/core/src/main/res/values-b+sr+Latn/strings.xml
Normal 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>
|
||||
202
app/core/src/main/res/values-be/strings.xml
Normal file
202
app/core/src/main/res/values-be/strings.xml
Normal 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>
|
||||
238
app/core/src/main/res/values-bg/strings.xml
Normal file
238
app/core/src/main/res/values-bg/strings.xml
Normal 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>
|
||||
234
app/core/src/main/res/values-bn/strings.xml
Normal file
234
app/core/src/main/res/values-bn/strings.xml
Normal 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>
|
||||
211
app/core/src/main/res/values-ca/strings.xml
Normal file
211
app/core/src/main/res/values-ca/strings.xml
Normal 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">Sí</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
Loading…
Add table
Add a link
Reference in a new issue