Separate core codebase into its own module

- Separate UI specific code and resources outside of the core
  application logic
- Allow most of the code to move forward and use KSP for annotation
  processing and isolate rotton code that is stuck with databinding
- Make full UI rewrite more feasible
This commit is contained in:
topjohnwu
2024-07-04 00:02:42 -07:00
parent f90c548f27
commit 3e38b8fed1
223 changed files with 344 additions and 318 deletions

4
app/core/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/build
src/*/assets
src/*/jniLibs
src/*/resources

73
app/core/build.gradle.kts Normal file
View File

@@ -0,0 +1,73 @@
plugins {
id("com.android.library")
kotlin("android")
kotlin("plugin.parcelize")
id("com.google.devtools.ksp")
}
setupCoreLib()
ksp {
arg("room.generateKotlin", "true")
}
android {
namespace = "com.topjohnwu.magisk.core"
defaultConfig {
vectorDrawables.useSupportLibrary = true
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)
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64", "riscv64")
debugSymbolLevel = "FULL"
}
}
buildFeatures {
aidl = true
buildConfig = true
}
}
dependencies {
api(project(":app:shared"))
api("com.jakewharton.timber:timber:5.0.1")
api("io.noties.markwon:core:4.6.2")
implementation("org.bouncycastle:bcpkix-jdk18on:1.78.1")
implementation("org.apache.commons:commons-compress:1.26.2")
val vLibsu = "6.0.0"
api("com.github.topjohnwu.libsu:core:${vLibsu}")
api("com.github.topjohnwu.libsu:service:${vLibsu}")
api("com.github.topjohnwu.libsu:nio:${vLibsu}")
val vRetrofit = "2.11.0"
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
val vOkHttp = "4.12.0"
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}")
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
val vMoshi = "1.15.1"
implementation("com.squareup.moshi:moshi:${vMoshi}")
ksp("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
val vRoom = "2.6.1"
implementation("androidx.room:room-runtime:${vRoom}")
implementation("androidx.room:room-ktx:${vRoom}")
ksp("androidx.room:room-compiler:${vRoom}")
api("androidx.appcompat:appcompat:1.7.0")
api("com.google.android.material:material:1.12.0")
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.collection:collection-ktx:1.4.0")
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
implementation("androidx.lifecycle:lifecycle-process:2.8.3")
}

View File

@@ -0,0 +1,73 @@
<?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"
android:multiArch="true"
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
tools:remove="android:appComponentFactory">
<receiver
android:name=".Receiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED" />
<action android:name="android.intent.action.UID_REMOVED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<service
android:name=".Service"
android:exported="false"
android:enabled="@bool/enable_fg_service"
android:foregroundServiceType="dataSync" />
<service
android:name=".JobService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
<provider
android:name=".Provider"
android:authorities="${applicationId}.provider"
android:directBootAware="true"
android:exported="false"
android:grantUriPermissions="true" />
<!-- We don't invalidate Room -->
<service
android:name="androidx.room.MultiInstanceInvalidationService"
tools:node="remove" />
<!-- We handle initialization ourselves -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
<!-- We handle profile installation ourselves -->
<receiver
android:name="androidx.profileinstaller.ProfileInstallReceiver"
tools:node="remove" />
</application>
</manifest>

View File

@@ -0,0 +1,9 @@
// 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();
}

View File

@@ -0,0 +1,132 @@
package com.topjohnwu.magisk.core
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.res.Configuration
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.di.ServiceLocator
import com.topjohnwu.magisk.core.utils.NetworkObserver
import com.topjohnwu.magisk.core.utils.ProcessLifecycle
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.core.utils.ShellInit
import com.topjohnwu.magisk.core.utils.refreshLocale
import com.topjohnwu.magisk.core.utils.setConfig
import com.topjohnwu.magisk.view.Notifications
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
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
}
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 attachBaseContext(context: Context) {
// Get the actual ContextImpl
val app: Application
val base: Context
if (context is Application) {
app = context
base = context.baseContext
AppApkPath = StubApk.current(base).path
} else {
app = this
base = context
AppApkPath = base.packageResourcePath
}
super.attachBaseContext(base)
ServiceLocator.context = base
app.registerActivityLifecycleCallbacks(ActivityTracker)
val shellBuilder = Shell.Builder.create()
.setFlags(Shell.FLAG_MOUNT_MASTER)
.setInitializers(ShellInit::class.java)
.setContext(base)
.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) {}
refreshLocale()
resources.patch()
Notifications.setup()
}
override fun onCreate() {
super.onCreate()
ProcessLifecycle.init(this)
NetworkObserver.init(this)
if (!BuildConfig.DEBUG && !isRunningAsStub) {
GlobalScope.launch(Dispatchers.IO) {
ProfileInstaller.writeProfile(this@App)
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
if (resources.configuration.diff(newConfig) != 0) {
resources.setConfig(newConfig)
}
if (!isRunningAsStub)
super.onConfigurationChanged(newConfig)
}
}
object ActivityTracker : Application.ActivityLifecycleCallbacks {
val foreground: Activity? get() = ref.get()
@Volatile
private var ref = WeakReference<Activity>(null)
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 onActivityCreated(activity: Activity, bundle: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
}

View File

@@ -0,0 +1,191 @@
package com.topjohnwu.magisk.core
import android.annotation.SuppressLint
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import com.topjohnwu.magisk.core.di.AppContext
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.repository.DBConfig
import com.topjohnwu.magisk.core.repository.PreferenceConfig
import com.topjohnwu.magisk.core.utils.refreshLocale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.runBlocking
import java.io.File
import java.io.IOException
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
private val prefsFile = File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
@SuppressLint("ApplySharedPref")
fun getPrefsFile(): File {
prefs.edit().remove(Key.ASKED_HOME).commit()
return prefsFile
}
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 CHECK_UPDATES = "check_update"
const val UPDATE_CHANNEL = "update_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"
}
object Value {
// 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
// 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 = intArrayOf(0, -1, 10, 20, 30, 60)
}
private val defaultChannel =
if (BuildConfig.DEBUG)
Value.DEBUG_CHANNEL
else if (Const.APP_IS_CANARY)
Value.CANARY_CHANNEL
else
Value.DEFAULT_CHANNEL
@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, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
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 preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
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
refreshLocale()
}
var zygisk by dbSettings(Key.ZYGISK, false)
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)
private const val SU_FINGERPRINT = "su_fingerprint"
fun load(pkg: String?) {
// Only try to load prefs when fresh install and a previous package name is set
if (pkg != null && prefs.all.isEmpty()) {
runBlocking {
try {
context.contentResolver
.openInputStream(Provider.preferencesUri(pkg))
?.writeTo(prefsFile, dispatcher = Dispatchers.Unconfined)
} catch (ignored: IOException) {}
}
return
}
prefs.edit {
// Settings migration
if (prefs.getBoolean(SU_FINGERPRINT, false))
suBiometric = true
remove(SU_FINGERPRINT)
prefs.getString(Key.UPDATE_CHANNEL, null).also {
if (it == null ||
it.toInt() > Value.DEBUG_CHANNEL ||
it.toInt() < Value.DEFAULT_CHANNEL) {
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
package com.topjohnwu.magisk.core
import android.os.Build
import android.os.Process
@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 MAGISK_PATH = "/data/adb/modules"
const val TMPDIR = "/dev/tmp"
const val MAGISK_LOG = "/cache/magisk.log"
// Misc
val USER_ID = Process.myUid() / 100000
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.APP_VERSION_CODE)
object Version {
const val MIN_VERSION = "v22.0"
const val MIN_VERCODE = 22000
fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary()
fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary()
fun isCanary() = isCanary(Info.env.versionCode)
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
}
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"
val CHANGELOG_URL = if (APP_IS_CANARY) Info.remote.magisk.note
else "https://topjohnwu.github.io/Magisk/releases/${BuildConfig.APP_VERSION_CODE}.md"
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
const val GITHUB_API_URL = "https://api.github.com/"
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk-files/"
const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/"
}
object Key {
// intents
const val OPEN_SECTION = "section"
const val PREV_PKG = "prev_pkg"
}
object Value {
const val FLASH_ZIP = "flash"
const val PATCH_FILE = "patch"
const val FLASH_MAGISK = "magisk"
const val FLASH_INACTIVE_SLOT = "slot"
const val UNINSTALL = "uninstall"
}
object Nav {
const val HOME = "home"
const val SETTINGS = "settings"
const val MODULES = "modules"
const val SUPERUSER = "superuser"
}
}

View File

@@ -0,0 +1,73 @@
@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.AssetManager
import android.content.res.Configuration
import android.content.res.Resources
import android.util.DisplayMetrics
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.di.AppContext
import com.topjohnwu.magisk.core.ktx.unwrap
import com.topjohnwu.magisk.core.utils.syncLocale
lateinit var AppApkPath: String
fun Resources.addAssetPath(path: String) = StubApk.addAssetPath(this, path)
fun Resources.patch(): Resources {
if (isRunningAsStub)
addAssetPath(AppApkPath)
syncLocale()
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 createNewResources(): Resources {
val asset = AssetManager::class.java.newInstance()
val config = Configuration(AppContext.resources.configuration)
val metrics = DisplayMetrics()
metrics.setTo(AppContext.resources.displayMetrics)
val res = Resources(asset, metrics, config)
res.addAssetPath(AppApkPath)
return res
}
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.drawable.ic_device,
R.drawable.ic_more,
R.drawable.ic_magisk_delete,
R.drawable.ic_refresh_data_md2,
R.drawable.ic_order_date,
R.drawable.ic_order_name,
R.array.allow_timeout,
)

View File

@@ -0,0 +1,116 @@
package com.topjohnwu.magisk.core
import android.app.KeyguardManager
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.di.AppContext
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
val EMPTY_REMOTE = UpdateInfo()
var remote = EMPTY_REMOTE
suspend fun getRemote(svc: NetworkService): UpdateInfo? {
return if (remote === EMPTY_REMOTE) {
svc.fetchUpdate()?.apply { remote = this }
} else remote
}
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
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
@JvmStatic val isFDE get() = crypto == "block"
@JvmStatic var ramdisk = false
private set
private var crypto = ""
var hasGMS = true
val isEmulator =
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")
// Default presets
Config.recovery = getBool("RECOVERYMODE")
Config.keepVerity = getBool("KEEPVERITY")
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
}
}

View File

@@ -0,0 +1,103 @@
package com.topjohnwu.magisk.core
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Notification
import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobScheduler
import android.content.Context
import androidx.core.content.getSystemService
import com.topjohnwu.magisk.core.base.BaseJobService
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.core.download.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
) : DownloadEngine.Session {
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) {
ServiceLocator.networkService.fetchUpdate()?.let {
Info.remote = it
if (Info.env.isActive && BuildConfig.APP_VERSION_CODE < it.magisk.versionCode)
Notifications.updateAvailable()
jobFinished(params, false)
}
}
return true
}
companion object {
fun schedule(context: Context) {
val scheduler = context.getSystemService<JobScheduler>() ?: return
if (Config.checkUpdate) {
val cmp = JobService::class.java.cmp(context.packageName)
val info = JobInfo.Builder(Const.ID.CHECK_UPDATE_JOB_ID, cmp)
.setPeriodic(TimeUnit.HOURS.toMillis(12))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setRequiresDeviceIdle(true)
.build()
scheduler.schedule(info)
} else {
scheduler.cancel(Const.ID.CHECK_UPDATE_JOB_ID)
}
}
}
}

View File

@@ -0,0 +1,34 @@
package com.topjohnwu.magisk.core
import android.net.Uri
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
import com.topjohnwu.magisk.core.base.BaseProvider
import com.topjohnwu.magisk.core.su.SuCallbackHandler
import com.topjohnwu.magisk.core.su.TestHandler
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 -> TestHandler.run(method)
}
}
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
return when (uri.encodedPath ?: return null) {
"/prefs_file" -> ParcelFileDescriptor.open(Config.getPrefsFile(), MODE_READ_ONLY)
else -> super.openFile(uri, mode)
}
}
companion object {
fun preferencesUri(pkg: String): Uri =
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
}
}

View File

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

View File

@@ -0,0 +1,38 @@
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.Subject
class Service : BaseService(), DownloadEngine.Session {
private var mEngine: DownloadEngine? = null
override val context get() = this
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
if (intent.action == DownloadEngine.ACTION) {
IntentCompat
.getParcelableExtra(intent, DownloadEngine.SUBJECT_KEY, Subject::class.java)
?.let { subject ->
val engine = mEngine ?: DownloadEngine(this).also { mEngine = it }
engine.download(subject)
}
}
return START_NOT_STICKY
}
override fun attachNotification(id: Int, builder: Notification.Builder) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
startForeground(id, builder.build())
}
override fun onDownloadComplete() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
}
}

View File

@@ -0,0 +1,139 @@
package com.topjohnwu.magisk.core.base
import android.Manifest.permission.POST_NOTIFICATIONS
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
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.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts.GetContent
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.appcompat.app.AppCompatActivity
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.isRunningAsStub
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
import com.topjohnwu.magisk.core.wrap
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
fun onActivityLaunch() {}
// Make the result type explicitly non-null
override fun onActivityResult(result: Uri)
}
interface UntrackedActivity
abstract class BaseActivity : AppCompatActivity() {
private var permissionCallback: ((Boolean) -> Unit)? = null
private val requestPermission = registerForActivityResult(RequestPermission()) {
permissionCallback?.invoke(it)
permissionCallback = null
}
private var installCallback: ((Boolean) -> Unit)? = null
private val requestInstall = registerForActivityResult(RequestInstall()) {
installCallback?.invoke(it)
installCallback = null
}
var authenticateCallback: ((Boolean) -> Unit)? = null
val requestAuthenticate = registerForActivityResult(RequestAuthentication()) {
authenticateCallback?.invoke(it)
authenticateCallback = null
}
private var contentCallback: ContentResultCallback? = null
private val getContent = registerForActivityResult(GetContent()) {
if (it != null) contentCallback?.onActivityResult(it)
contentCallback = null
}
private val mReferrerField by lazy(LazyThreadSafetyMode.NONE) {
Activity::class.java.reflectField("mReferrer")
}
val realCallingPackage: String? get() {
callingPackage?.let { return it }
mReferrerField.get(this)?.let { return it as String }
return null
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base.wrap())
}
override fun onCreate(savedInstanceState: Bundle?) {
if (isRunningAsStub) {
// Overwrite private members to avoid nasty "false" stack traces being logged
val delegate = delegate
val clz = delegate.javaClass
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
}
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
super.onCreate(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
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 getContent(type: String, callback: ContentResultCallback) {
contentCallback = callback
try {
getContent.launch(type)
callback.onActivityLaunch()
} catch (e: ActivityNotFoundException) {
toast(R.string.app_not_found, Toast.LENGTH_SHORT)
}
}
override fun recreate() {
startActivity(Intent().setComponent(intent.component))
finish()
}
fun relaunch() {
startActivity(Intent(intent).setFlags(0))
finish()
}
companion object {
private const val CONTENT_CALLBACK_KEY = "content_callback"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
package com.topjohnwu.magisk.core.data
import com.topjohnwu.magisk.core.model.BranchInfo
import com.topjohnwu.magisk.core.model.ModuleJson
import com.topjohnwu.magisk.core.model.UpdateInfo
import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Path
import retrofit2.http.Streaming
import retrofit2.http.Url
private const val BRANCH = "branch"
private const val REPO = "repo"
private const val FILE = "file"
interface GithubPageServices {
@GET
suspend fun fetchUpdateJSON(@Url file: String): UpdateInfo
}
interface RawServices {
@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
}
interface GithubApiServices {
@GET("repos/{$REPO}/branches/{$BRANCH}")
@Headers("Accept: application/vnd.github.v3+json")
suspend fun fetchBranch(
@Path(REPO, encoded = true) repo: String,
@Path(BRANCH) branch: String
): BranchInfo
}

View File

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

View File

@@ -0,0 +1,47 @@
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 {
suspend fun <R> exec(
query: String,
mapper: suspend (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(it) }
}
}
}
suspend inline fun exec(query: String) {
exec(query) {}
}
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()
else -> "\"$it\""
}
}
return "($keys) VALUES($values)"
}
object Table {
const val POLICY = "policies"
const val SETTINGS = "settings"
const val STRINGS = "strings"
}
}

View File

@@ -0,0 +1,53 @@
package com.topjohnwu.magisk.core.data.magiskdb
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.di.AppContext
import com.topjohnwu.magisk.core.model.su.SuPolicy
import java.util.concurrent.TimeUnit
class PolicyDao : MagiskDB() {
suspend fun deleteOutdated() {
val nowSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
val query = "DELETE FROM ${Table.POLICY} WHERE " +
"(until > 0 AND until < $nowSeconds) 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 * 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 * 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["policy"]?.toInt()?.let { policy.policy = it }
map["until"]?.toLong()?.let { policy.until = it }
map["logging"]?.toInt()?.let { policy.logging = it != 0 }
map["notification"]?.toInt()?.let { policy.notification = it != 0 }
return policy
}
}

View File

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

View File

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

View File

@@ -0,0 +1,99 @@
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.Info
import com.topjohnwu.magisk.core.utils.currentLocale
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", currentLocale.toLanguageTag())
chain.proceed(request.build())
}
if (!ProviderInstaller.install(context)) {
Info.hasGMS = false
}
return builder.build()
}
fun createMoshiConverterFactory(): MoshiConverterFactory {
val moshi = Moshi.Builder().build()
return MoshiConverterFactory.create(moshi)
}
fun createRetrofit(okHttpClient: OkHttpClient): Retrofit.Builder {
return Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(createMoshiConverterFactory())
.client(okHttpClient)
}
inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseUrl: String): T {
return retrofitBuilder
.baseUrl(baseUrl)
.build()
.create(T::class.java)
}

View File

@@ -0,0 +1,60 @@
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.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
val AppContext: Context inline get() = ServiceLocator.context
@SuppressLint("StaticFieldLeak")
object ServiceLocator {
lateinit var context: Context
val deContext by lazy { context.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(context) }
val retrofit by lazy { createRetrofit(okhttp) }
val markwon by lazy { createMarkwon(context) }
val networkService by lazy {
NetworkService(
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
)
}
}
private fun createSuLogDatabase(context: Context) =
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
.addMigrations(SuLogDatabase.MIGRATION_1_2)
.fallbackToDestructiveMigration()
.build()
private fun createMarkwon(context: Context) =
Markwon.builder(context).textSetter { textView, spanned, bufferType, onComplete ->
textView.apply {
movementMethod = LinkMovementMethod.getInstance()
setSpannableFactory(NoCopySpannableFactory.getInstance())
setText(spanned, bufferType)
onComplete.run()
}
}.build()

View File

@@ -0,0 +1,373 @@
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.net.Uri
import android.os.Build
import android.os.Bundle
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.StubApk
import com.topjohnwu.magisk.core.ActivityTracker
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.JobService
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.cmp
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.intent
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.forEach
import com.topjohnwu.magisk.core.ktx.set
import com.topjohnwu.magisk.core.ktx.withStreams
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.utils.APKInstall
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.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
/**
* 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(
private val session: Session
) {
interface Session {
val context: Context
fun attachNotification(id: Int, builder: Notification.Builder)
fun onDownloadComplete()
}
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 createIntent(context: Context, subject: Subject) =
if (Build.VERSION.SDK_INT >= 34) {
context.intent<com.topjohnwu.magisk.core.Receiver>()
.setAction(ACTION)
.putExtra(SUBJECT_KEY, subject)
} else {
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
val intent = createIntent(context, subject)
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.
PendingIntent.getBroadcast(context, REQUEST_CODE, intent, flag)
} else if (Build.VERSION.SDK_INT >= 26) {
PendingIntent.getForegroundService(context, REQUEST_CODE, intent, flag)
} else {
PendingIntent.getService(context, REQUEST_CODE, intent, flag)
}
}
@SuppressLint("InlinedApi")
fun startWithActivity(activity: BaseActivity, subject: Subject) {
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
// Always download regardless of notification permission status
start(activity.applicationContext, subject)
}
}
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 if (Build.VERSION.SDK_INT >= 26) {
context.startForegroundService(createIntent(context, subject))
} else {
context.startService(createIntent(context, subject))
}
}
}
fun download(subject: Subject) {
notifyUpdate(subject.notifyId)
CoroutineScope(job + Dispatchers.IO).launch {
try {
val stream = network.fetchFile(subject.url).toProgressStream(subject)
when (subject) {
is Subject.App -> handleApp(stream, subject)
is Subject.Module -> handleModule(stream, subject.file)
else -> stream.copyAndClose(subject.file.outputStream())
}
val activity = ActivityTracker.foreground
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
session.attachNotification(attachedId, builder)
}
private val notifications = SparseArrayCompat<Notification.Builder>()
private var attachedId = -1
private val job = Job()
private val context get() = session.context
private val network get() = ServiceLocator.networkService
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) }
}
private fun attachNotification(id: Int, notification: Notification.Builder) {
attachedId = id
session.attachNotification(id, notification)
}
@Synchronized
private fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
val notification = (notifications[id] ?: Notifications.startProgress("").also {
notifications[id] = it
}).apply(editor)
if (attachedId < 0)
attachNotification(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)
attachNotification(anotherId, notification)
} else {
// No more notifications left, terminate the session
attachedId = -1
session.onDownloadComplete()
}
}
}
Notifications.mgr.cancel(id)
return n
}
private 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 zf = ZipFile(updateApk)
val apk = context.cachedFile("stub.apk")
apk.delete()
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
zf.close()
// Patch and install
subject.intent = HideAPK.upgrade(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()
}
}
private suspend fun handleModule(src: InputStream, file: Uri) {
val input = ZipInputStream(src)
val output = ZipOutputStream(file.outputStream())
withStreams(input, output) { zin, zout ->
zout.putNextEntry(ZipEntry("META-INF/"))
zout.putNextEntry(ZipEntry("META-INF/com/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
context.assets.open("module_installer.sh").use { it.copyAll(zout) }
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
zout.write("#MAGISK\n".toByteArray())
zin.forEach { entry ->
val path = entry.name
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
zout.putNextEntry(ZipEntry(path))
if (!entry.isDirectory) {
zin.copyAll(zout)
}
}
}
}
}
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()
}
}
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
val max = contentLength()
val total = max.toFloat() / 1048576
val id = subject.notifyId
notifyUpdate(id) { it.setContentTitle(subject.title) }
return ProgressInputStream(byteStream()) {
val progress = it.toFloat() / 1048576
notifyUpdate(id) { notification ->
if (max > 0) {
broadcast(progress / total, subject)
notification
.setProgress(max.toInt(), it.toInt(), false)
.setContentText("%.2f / %.2f MB".format(progress, total))
} else {
broadcast(-1f, subject)
notification.setContentText("%.2f MB / ??".format(progress))
}
}
}
}
}

View File

@@ -0,0 +1,83 @@
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.MagiskJson
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
sealed 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
@Parcelize
class Module(
private val module: OnlineModule,
override val autoLaunch: Boolean,
override val notifyId: Int = Notifications.nextId()
) : Subject() {
override val url: String get() = module.zipUrl
override val title: String get() = module.downloadFilename
@IgnoredOnParcel
override val file by lazy {
MediaStoreUtils.getFile(title).uri
}
@IgnoredOnParcel
var piCreator: ((Context, Uri) -> PendingIntent)? = null
override fun pendingIntent(context: Context) = piCreator?.invoke(context, file)
}
@Parcelize
class App(
private val json: MagiskJson = Info.remote.magisk,
override val notifyId: Int = Notifications.nextId()
) : Subject() {
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
override val url: String get() = json.link
@IgnoredOnParcel
override val file by lazy {
MediaStoreUtils.getFile("${title}.apk").uri
}
@IgnoredOnParcel
var intent: Intent? = null
override fun pendingIntent(context: Context) = intent?.toPending(context)
}
@Parcelize
class Test(
override val notifyId: Int = Notifications.nextId(),
override val title: String = UUID.randomUUID().toString().substring(0, 6)
) : Subject() {
override val url get() = "https://link.testfile.org/250MB"
override val file get() = File("/dev/null").toUri()
override val autoLaunch get() = false
}
@SuppressLint("InlinedApi")
protected fun Intent.toPending(context: Context): PendingIntent {
return PendingIntent.getActivity(context, notifyId, this,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT)
}
}

View File

@@ -0,0 +1,150 @@
package com.topjohnwu.magisk.core.ktx
import android.annotation.SuppressLint
import android.app.Activity
import android.content.*
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
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.appcompat.content.res.AppCompatResources
import androidx.core.content.getSystemService
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.superuser.internal.UiThreadHandler
import java.io.File
import kotlin.String
fun Context.getBitmap(id: Int): Bitmap {
var drawable = AppCompatResources.getDrawable(this, 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)
val config = Configuration()
config.setLocale(currentLocale)
res.updateConfiguration(config, res.displayMetrics)
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.obj?.getAppProcess(pid)
if (proc == null) {
if (uid == Process.SHELL_UID) {
// It is possible that some apps installed are sharing UID with shell.
// We will not be able to find a package from the active process list,
// because the client is forked from ADB shell, not any app process.
return getPackageInfo("com.android.shell", flag)
}
} else if (uid == proc.uid) {
return getPackageInfo(proc.pkgList[0], flag)
}
return null
}
if (pkgs.size == 1) {
return getPackageInfo(pkgs[0], flag)
}
throw PackageManager.NameNotFoundException()
}
fun Context.registerRuntimeReceiver(receiver: BroadcastReceiver, filter: IntentFilter) {
APKInstall.registerReceiver(this, receiver, filter)
}
fun Context.selfLaunchIntent(): Intent {
val pm = packageManager
val intent = pm.getLaunchIntentForPackage(packageName)!!
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
return intent
}
fun Context.toast(msg: CharSequence, duration: Int) {
UiThreadHandler.run { Toast.makeText(this, msg, duration).show() }
}
fun Context.toast(resId: Int, duration: Int) {
UiThreadHandler.run { Toast.makeText(this, resId, duration).show() }
}

View File

@@ -0,0 +1,110 @@
package com.topjohnwu.magisk.core.ktx
import androidx.collection.SparseArrayCompat
import com.topjohnwu.magisk.core.utils.currentLocale
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.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.lang.reflect.Field
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Collections
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
inline fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
var entry: ZipEntry? = nextEntry
while (entry != null) {
callback(entry)
entry = nextEntry
}
}
inline fun <In : InputStream, Out : OutputStream> withStreams(
inStream: In,
outStream: Out,
withBoth: (In, Out) -> Unit
) {
inStream.use { reader ->
outStream.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
) = withStreams(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: DateFormat) = format.format(this).orEmpty()
// Some devices don't allow filenames containing ":"
val timeFormatStandard by lazy {
SimpleDateFormat(
"yyyy-MM-dd'T'HH.mm.ss",
currentLocale
)
}
val timeDateFormat: DateFormat by lazy {
DateFormat.getDateTimeInstance(
DateFormat.DEFAULT,
DateFormat.DEFAULT,
currentLocale
)
}

View File

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

View File

@@ -0,0 +1,37 @@
package com.topjohnwu.magisk.core.model
import android.os.Parcelable
import com.squareup.moshi.JsonClass
import kotlinx.parcelize.Parcelize
@JsonClass(generateAdapter = true)
data class UpdateInfo(
val magisk: MagiskJson = MagiskJson(),
)
@Parcelize
@JsonClass(generateAdapter = true)
data class MagiskJson(
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 CommitInfo(
val sha: String
)
@JsonClass(generateAdapter = true)
data class BranchInfo(
val commit: CommitInfo
)

View File

@@ -0,0 +1,136 @@
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
import java.util.*
data class LocalModule(
private val path: String,
) : 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 = RootUtils.fs.getFile(path, "remove")
private val disableFile = RootUtils.fs.getFile(path, "disable")
private val updateFile = RootUtils.fs.getFile(path, "update")
private val riruFolder = RootUtils.fs.getFile(path, "riru")
private val zygiskFolder = RootUtils.fs.getFile(path, "zygisk")
private val unloaded = RootUtils.fs.getFile(zygiskFolder, "unloaded")
val updated: Boolean get() = updateFile.exists()
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
val isZygisk: Boolean get() = zygiskFolder.exists()
val zygiskUnloaded: Boolean get() = unloaded.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 < $path/module.prop").exec().out)
}
if (id.isEmpty()) {
val sep = path.lastIndexOf('/')
id = path.substring(sep + 1)
}
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.MAGISK_PATH).exists()
suspend fun installed() = withContext(Dispatchers.IO) {
RootUtils.fs.getFile(Const.MAGISK_PATH)
.listFiles()
.orEmpty()
.filter { !it.isFile && !it.isHidden }
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
.sortedBy { it.name.lowercase(Locale.ROOT) }
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,73 @@
package com.topjohnwu.magisk.core.model.su
import android.content.pm.PackageInfo
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: PackageInfo,
toUid: Int,
fromPid: Int,
command: String,
policy: Int,
target: Int,
context: String,
gids: String,
): SuLog {
val appInfo = info.applicationInfo
return SuLog(
fromUid = appInfo.uid,
toUid = toUid,
fromPid = fromPid,
packageName = getNameForUid(appInfo.uid)!!,
appName = appInfo.getLabel(this),
command = command,
action = policy,
target = target,
context = context,
gids = gids,
)
}
fun createSuLog(
fromUid: Int,
toUid: Int,
fromPid: Int,
command: String,
policy: Int,
target: Int,
context: String,
gids: String,
): SuLog {
return SuLog(
fromUid = fromUid,
toUid = toUid,
fromPid = fromPid,
packageName = "[UID] $fromUid",
appName = "[UID] $fromUid",
command = command,
action = policy,
target = target,
context = context,
gids = gids,
)
}

View File

@@ -0,0 +1,22 @@
package com.topjohnwu.magisk.core.model.su
class SuPolicy(val uid: Int) {
companion object {
const val INTERACTIVE = 0
const val DENY = 1
const val ALLOW = 2
}
var policy: Int = INTERACTIVE
var until: Long = -1L
var logging: Boolean = true
var notification: Boolean = true
fun toMap(): MutableMap<String, Any> = mutableMapOf(
"uid" to uid,
"policy" to policy,
"until" to until,
"logging" to logging,
"notification" to notification
)
}

View File

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

View File

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

View File

@@ -0,0 +1,69 @@
package com.topjohnwu.magisk.core.repository
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.CANARY_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.GithubPageServices
import com.topjohnwu.magisk.core.data.RawServices
import retrofit2.HttpException
import timber.log.Timber
import java.io.IOException
class NetworkService(
private val pages: GithubPageServices,
private val raw: RawServices
) {
suspend fun fetchUpdate() = safe {
var info = when (Config.updateChannel) {
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
BETA_CHANNEL -> fetchBetaUpdate()
CANARY_CHANNEL -> fetchCanaryUpdate()
DEBUG_CHANNEL -> fetchDebugUpdate()
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
else -> throw IllegalArgumentException()
}
if (info.magisk.versionCode < Info.env.versionCode &&
Config.updateChannel == DEFAULT_CHANNEL) {
Config.updateChannel = BETA_CHANNEL
info = fetchBetaUpdate()
}
info
}
// UpdateInfo
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
private suspend fun fetchDebugUpdate() = pages.fetchUpdateJSON("debug.json")
private suspend fun fetchCustomUpdate(url: String) = pages.fetchUpdateJSON(url)
private inline fun <T> safe(factory: () -> T): T? {
return try {
if (Info.isConnected.value == true)
factory()
else
null
} catch (e: Exception) {
Timber.e(e)
null
}
}
private inline fun <T> wrap(factory: () -> T): T {
return try {
factory()
} catch (e: HttpException) {
throw IOException(e)
}
}
// Fetch files
suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) }
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
suspend fun fetchModuleJson(url: String) = wrap { raw.fetchModuleJson(url) }
}

View File

@@ -0,0 +1,230 @@
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: Float,
commit: Boolean = false
) = FloatProperty(name, default, commit)
fun preference(
name: String,
default: Int,
commit: Boolean = false
) = IntProperty(name, default, commit)
fun preference(
name: String,
default: Long,
commit: Boolean = false
) = LongProperty(name, default, commit)
fun preference(
name: String,
default: String,
commit: Boolean = false
) = StringProperty(name, default, commit)
fun preference(
name: String,
default: Set<String>,
commit: Boolean = false
) = StringSetProperty(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 FloatProperty(
private val name: String,
private val default: Float,
private val commit: Boolean
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Float> {
override operator fun getValue(
thisRef: PreferenceConfig,
property: KProperty<*>
): Float {
val prefName = name.ifBlank { property.name }
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceConfig,
property: KProperty<*>,
value: Float
) {
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 LongProperty(
private val name: String,
private val default: Long,
private val commit: Boolean
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Long> {
override operator fun getValue(
thisRef: PreferenceConfig,
property: KProperty<*>
): Long {
val prefName = name.ifBlank { property.name }
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceConfig,
property: KProperty<*>,
value: Long
) {
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) }
}
}
class StringSetProperty(
private val name: String,
private val default: Set<String>,
private val commit: Boolean
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Set<String>> {
override operator fun getValue(
thisRef: PreferenceConfig,
property: KProperty<*>
): Set<String> {
val prefName = name.ifBlank { property.name }
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceConfig,
property: KProperty<*>,
value: Set<String>
) {
val prefName = name.ifBlank { property.name }
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

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

View File

@@ -0,0 +1,108 @@
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 = 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: Int) {
val until = if (time > 0)
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) +
TimeUnit.MINUTES.toSeconds(time.toLong())
else
time.toLong()
policy.policy = action
policy.until = until
withContext(Dispatchers.IO) {
try {
DataOutputStream(FileOutputStream(output)).use {
it.writeInt(policy.policy)
it.flush()
}
} catch (e: IOException) {
Timber.e(e)
}
if (until >= 0) {
policyDB.update(policy)
}
}
}
}

View File

@@ -0,0 +1,80 @@
package com.topjohnwu.magisk.core.su
import android.os.Bundle
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.di.ServiceLocator
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 kotlinx.coroutines.Runnable
import kotlinx.coroutines.runBlocking
import timber.log.Timber
object TestHandler {
object LogList : CallbackList<String>(Runnable::run) {
override fun onAddElement(e: String) {
Timber.i(e)
}
}
fun run(method: String): Bundle {
var reason: String? = null
fun prerequisite(): Boolean {
// Make sure the Magisk app can get root
val shell = Shell.getShell()
if (!shell.isRoot) {
reason = "shell not root"
return false
}
// Make sure the root service is running
RootUtils.Connection.await()
return true
}
fun setup(): Boolean {
return runBlocking {
MagiskInstaller.Emulator(LogList, LogList).exec()
}
}
fun test(): Boolean {
// Make sure Zygisk works correctly
if (!Info.isZygiskEnabled) {
reason = "zygisk not enabled"
return false
}
// Clear existing grant for ADB shell
runBlocking {
ServiceLocator.policyDB.delete(2000)
Config.suAutoResponse = Config.Value.SU_AUTO_ALLOW
Config.prefs.edit().commit()
}
return true
}
val result = prerequisite() && runCatching {
when (method) {
"setup" -> setup()
"test" -> test()
else -> {
reason = "unknown method"
false
}
}
}.getOrElse {
reason = it.stackTraceToString()
false
}
return Bundle().apply {
putBoolean("result", result)
if (reason != null) putString("reason", reason)
}
}
}

View File

@@ -0,0 +1,85 @@
package com.topjohnwu.magisk.core.tasks
import android.net.Uri
import androidx.core.net.toFile
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.di.AppContext
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.magisk.core.utils.unzip
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
}
}
}
val isValid = try {
zipFile.unzip(installDir, "META-INF/com/google/android", true)
val script = File(installDir, "updater-script")
script.readText().contains("#MAGISK")
} catch (e: IOException) {
console.add("! Unzip error")
throw e
}
if (!isValid) {
console.add("! This zip is not a Magisk module!")
return false
}
console.add("- Installing ${mUri.displayName}")
return Shell.cmd("sh $installDir/update-binary dummy 1 \'$zipFile\'")
.to(console, logs).exec().isSuccess
}
open suspend fun exec() = withContext(Dispatchers.IO) {
try {
if (!flash()) {
console.add("! Installation failed")
false
} else {
true
}
} catch (e: IOException) {
Timber.e(e)
false
} finally {
Shell.cmd("cd /", "rm -rf $installDir ${Const.TMPDIR}").submit()
}
}
}

View File

@@ -0,0 +1,271 @@
package com.topjohnwu.magisk.core.tasks
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.annotation.WorkerThread
import com.topjohnwu.magisk.StubApk
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.Provider
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.ktx.await
import com.topjohnwu.magisk.core.ktx.copyAndClose
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.utils.AXML
import com.topjohnwu.magisk.core.utils.Keygen
import com.topjohnwu.magisk.signing.JarMap
import com.topjohnwu.magisk.signing.SignApk
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.security.SecureRandom
import kotlin.random.asKotlinRandom
object HideAPK {
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
private const val ALPHADOTS = "$ALPHA....."
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
// 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 info = context.packageManager.getPackageArchiveInfo(apk.path, 0) ?: return false
val origLabel = info.applicationInfo.nonLocalizedLabel.toString()
try {
JarMap.open(apk, true).use { jar ->
val je = jar.getJarEntry(ANDROID_MANIFEST)
val xml = AXML(jar.getRawData(je))
val generator = classNameGenerator()
if (!xml.patchStrings {
for (i in it.indices) {
val s = it[i]
if (s.contains(APP_PACKAGE_NAME)) {
it[i] = s.replace(APP_PACKAGE_NAME, pkg)
} else if (s.contains(PLACEHOLDER)) {
it[i] = generator.next()
} else if (s == origLabel) {
it[i] = label.toString()
}
}
}) {
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 launchApp(activity: Activity, pkg: String) {
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
val self = activity.packageName
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
activity.grantUriPermission(pkg, Provider.preferencesUri(self), flag)
intent.putExtra(Const.Key.PREV_PKG, self)
activity.startActivity(intent)
activity.finish()
}
private suspend fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
val stub = File(activity.cacheDir, "stub.apk")
try {
activity.assets.open("stub.apk").writeTo(stub)
} catch (e: IOException) {
Timber.e(e)
return false
}
// Generate a new random package name and signature
val repack = File(activity.cacheDir, "patched.apk")
val pkg = genPackageName()
Config.keyStoreRaw = ""
if (!patch(activity, stub, FileOutputStream(repack), pkg, label))
return false
// Install and auto launch app
val session = APKInstall.startSession(activity, pkg, onFailure) {
launchApp(activity, pkg)
}
Config.suManager = pkg
val cmd = "adb_pm_install $repack $pkg"
if (Shell.cmd(cmd).exec().isSuccess) return true
try {
repack.inputStream().copyAndClose(session.openStream(activity))
} catch (e: IOException) {
Timber.e(e)
return false
}
session.waitIntent()?.let { activity.startActivity(it) } ?: return false
return true
}
@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 onFailure = Runnable {
dialog.dismiss()
activity.toast(R.string.failure, Toast.LENGTH_LONG)
}
val success = withContext(Dispatchers.IO) {
patchAndHide(activity, label, onFailure)
}
if (!success) onFailure.run()
}
@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()
}
val onFailure = Runnable {
dialog.dismiss()
activity.toast(R.string.failure, Toast.LENGTH_LONG)
}
val apk = StubApk.current(activity)
val session = APKInstall.startSession(activity, APP_PACKAGE_NAME, onFailure) {
launchApp(activity, APP_PACKAGE_NAME)
dialog.dismiss()
}
Config.suManager = ""
val cmd = "adb_pm_install $apk $APP_PACKAGE_NAME"
if (Shell.cmd(cmd).await().isSuccess) return
val success = withContext(Dispatchers.IO) {
try {
apk.inputStream().copyAndClose(session.openStream(activity))
} catch (e: IOException) {
Timber.e(e)
return@withContext false
}
session.waitIntent()?.let { activity.startActivity(it) } ?: return@withContext false
return@withContext true
}
if (!success) onFailure.run()
}
@WorkerThread
fun upgrade(context: Context, apk: File): Intent? {
val label = context.applicationInfo.nonLocalizedLabel
val pkg = context.packageName
val session = APKInstall.startSession(context)
session.openStream(context).use {
if (!patch(context, apk, it, pkg, label)) {
return null
}
}
return session.waitIntent()
}
}

View File

@@ -0,0 +1,685 @@
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 android.widget.Toast
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.R
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.copyAndClose
import com.topjohnwu.magisk.core.ktx.reboot
import com.topjohnwu.magisk.core.ktx.toast
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.Arrays
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 bootPath = (
"(RECOVERYMODE=${Config.recovery} " +
"SLOT=$slot find_boot_image; " +
"echo \$BOOTIMAGE)").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 magisk32 = File(installDir, "magisk32")
val entry = zf.getEntry("lib/$abi32/libmagisk.so")
zf.getInputStream(entry).writeTo(magisk32)
magisk32.setExecutable(true)
}
}
} 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 symlink magisk32 on 64-bit devices that supports 32-bit
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir")
.get(info) as String?
if (lib32 != null) {
Os.symlink("$lib32/libmagisk.so", "$installDir/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) }
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 TarArchiveEntry.decompressedStream(): InputStream {
val stream = if (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}")
entry.decompressedStream().copyAndCloseOut(bootItem.file.newOutputStream())
} else if (entry.name.contains("vbmeta.img")) {
val rawData = entry.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, bufferSize = 1024 * 1024)
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(), 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 = Arrays.copyOfRange(head, 257, 262)
srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) {
// tar file
outFile = MediaStoreUtils.getFile("$destName.tar")
outStream = TarArchiveOutputStream(outFile.uri.outputStream()).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().copyAndClose(outStream)
}
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
}
// 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 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
Shell.cmd("rm -rf $installDir").submit()
return false
}
companion object {
private var haveActiveSession = AtomicBoolean(false)
}
}
abstract class MagiskInstaller(
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
}
class Patch(
private val uri: Uri,
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstaller(console, logs) {
override suspend fun operations() = patchFile(uri)
}
class SecondSlot(
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstaller(console, logs) {
override suspend fun operations() = secondSlot()
}
class Direct(
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstaller(console, logs) {
override suspend fun operations() = direct()
}
class Emulator(
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstaller(console, logs) {
override suspend fun operations() = fixEnv()
}
class Uninstall(
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstallImpl(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 FixEnv(private val callback: () -> Unit) : MagiskInstallImpl(DummyList, DummyList) {
override suspend fun operations() = fixEnv()
override suspend fun exec(): Boolean {
val success = super.exec()
callback()
context.toast(
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
Toast.LENGTH_LONG
)
if (success)
UiThreadHandler.handler.postDelayed(5000) { reboot() }
return success
}
}
}

View File

@@ -0,0 +1,122 @@
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(patchFn: (Array<String>) -> Unit): 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()
patchFn(strArr)
// Write everything before string data, will patch values later
val baos = RawByteStream()
baos.write(bytes, 0, dataOff)
// Write string data
val offList = IntArray(count)
for (i in 0 until count) {
offList[i] = baos.size() - dataOff
val str = strArr[i]
baos.write(str.length.toShortBytes())
baos.write(str.toByteArray(UTF_16LE))
// Null terminate
baos.write(0)
baos.write(0)
}
baos.align()
val sizeDiff = baos.size() - start - size
val newBuffer = ByteBuffer.wrap(baos.buffer).order(LITTLE_ENDIAN)
// Patch XML size
newBuffer.putInt(CHUNK_SIZE_OFF, buffer.getInt(CHUNK_SIZE_OFF) + sizeDiff)
// Patch string pool size
newBuffer.putInt(start + CHUNK_SIZE_OFF, size + sizeDiff)
// Patch index table
newBuffer.position(start + STRING_INDICES_OFF)
val newIntBuf = newBuffer.asIntBuffer()
offList.forEach { newIntBuf.put(it) }
// Write the rest of the chunks
val nextOff = start + size
baos.write(bytes, nextOff, bytes.size - nextOff)
bytes = baos.toByteArray()
return true
}
private fun Int.toShortBytes(): ByteArray {
val b = ByteBuffer.allocate(2).order(LITTLE_ENDIAN)
b.putShort(this.toShort())
return b.array()
}
private class RawByteStream : ByteArrayOutputStream() {
val buffer: ByteArray get() = buf
fun align(alignment: Int = 4) {
val newCount = (count + alignment - 1) / alignment * alignment
for (i in 0 until (newCount - count))
write(0)
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,88 @@
@file:Suppress("DEPRECATION")
package com.topjohnwu.magisk.core.utils
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.content.res.Resources
import com.topjohnwu.magisk.core.ActivityTracker
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.createNewResources
import com.topjohnwu.magisk.core.di.AppContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.Locale
var currentLocale: Locale = Locale.getDefault()
@SuppressLint("ConstantLocale")
val defaultLocale: Locale = Locale.getDefault()
private var cachedLocales: Pair<Array<String>, Array<String>>? = null
suspend fun availableLocales() = cachedLocales ?:
withContext(Dispatchers.Default) {
val compareId = R.string.app_changelog
// Create a completely new resource to prevent cross talk over active configs
val res = createNewResources()
fun changeLocale(locale: Locale) {
res.configuration.setLocale(locale)
res.updateConfiguration(res.configuration, res.displayMetrics)
}
val locales = ArrayList<String>().apply {
// Add default locale
add("en")
// Add some special locales
add("zh-TW")
add("pt-BR")
// Then add all supported locales
addAll(Resources.getSystem().assets.locales)
}.map {
Locale.forLanguageTag(it)
}.distinctBy {
changeLocale(it)
res.getString(compareId)
}.sortedWith { a, b ->
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
}
changeLocale(defaultLocale)
val defName = res.getString(R.string.system_default)
val names = ArrayList<String>(locales.size + 1)
val values = ArrayList<String>(locales.size + 1)
names.add(defName)
values.add("")
locales.forEach { locale ->
names.add(locale.getDisplayName(locale))
values.add(locale.toLanguageTag())
}
(names.toTypedArray() to values.toTypedArray()).also { cachedLocales = it }
}
fun Resources.setConfig(config: Configuration) {
config.setLocale(currentLocale)
updateConfiguration(config, displayMetrics)
}
fun Resources.syncLocale() = setConfig(configuration)
fun refreshLocale() {
val localeConfig = Config.locale
currentLocale = when {
localeConfig.isEmpty() -> defaultLocale
else -> Locale.forLanguageTag(localeConfig)
}
Locale.setDefault(currentLocale)
AppContext.resources.syncLocale()
ActivityTracker.foreground?.recreate()
}

View File

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

View File

@@ -0,0 +1,80 @@
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 androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.ktx.registerRuntimeReceiver
class NetworkObserver(context: Context): DefaultLifecycleObserver {
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)
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}
override fun onStart(owner: LifecycleOwner) {
postCurrentState()
}
private fun postCurrentState() {
postValue(manager.getNetworkCapabilities(manager.activeNetwork)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) ?: false)
}
private fun postValue(b: Boolean) {
Info.remote = Info.EMPTY_REMOTE
Info.isConnected.postValue(b)
}
companion object {
fun init(context: Context): NetworkObserver {
return NetworkObserver(context).apply { postCurrentState() }
}
}
}

View File

@@ -0,0 +1,15 @@
package com.topjohnwu.magisk.core.utils;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleDispatcher;
import androidx.lifecycle.ProcessLifecycleOwner;
// Use Java to bypass Kotlin internal visibility modifier
public class ProcessLifecycle {
public static void init(@NonNull Context context) {
LifecycleDispatcher.init(context);
ProcessLifecycleOwner.init$lifecycle_process_release(context);
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,139 @@
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.Info
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.ipc.RootService
import com.topjohnwu.superuser.nio.FileSystemManager
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) {
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()
}
}
private inline fun <T> safe(default: T, block: () -> T): T {
return try {
block()
} catch (e: Throwable) {
// The process died unexpectedly
Timber.e(e)
default
}
}
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
}
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
var obj: IRootUtils? = null
get() {
Connection.await()
return field
}
private set
}
}

View File

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

View File

@@ -0,0 +1,50 @@
package com.topjohnwu.magisk.core.utils
import com.topjohnwu.magisk.core.ktx.copyAll
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 java.io.File
import java.io.IOException
import java.io.InputStream
@Throws(IOException::class)
suspend fun File.unzip(folder: File, path: String = "", junkPath: Boolean = false) {
ZipFile.Builder().setFile(this).get().use { zip ->
for (entry in zip.entries) {
if (!entry.name.startsWith(path) || entry.isDirectory) {
// Ignore directories, only create files
continue
}
val name = if (junkPath)
entry.name.substring(entry.name.lastIndexOf('/') + 1)
else
entry.name
val dest = File(folder, name)
dest.parentFile?.mkdirs()
dest.outputStream().use { out -> zip.getInputStream(entry).copyAll(out) }
}
}
}
@Throws(IOException::class)
suspend fun InputStream.unzip(folder: File, path: String = "", junkPath: Boolean = false) {
ZipArchiveInputStream(this).use { zin ->
var entry: ZipArchiveEntry
while (true) {
entry = zin.nextEntry ?: break
if (!entry.name.startsWith(path) || entry.isDirectory) {
// Ignore directories, only create files
continue
}
val name = if (junkPath)
entry.name.substring(entry.name.lastIndexOf('/') + 1)
else
entry.name
val dest = File(folder, name)
dest.parentFile?.mkdirs()
dest.outputStream().use { out -> zin.copyAll(out) }
}
}
}

View File

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

View File

@@ -0,0 +1,30 @@
package com.topjohnwu.magisk.signing;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
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);
}
}

View File

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

View File

@@ -0,0 +1,567 @@
package com.topjohnwu.magisk.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.
ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
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 = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
v1SignedApkBuf.reset();
ByteBuffer[] outputChunks;
List<ApkSignerV2.SignerConfig> signerConfigs = createV2SignerConfigs(privateKey, publicKey,
new String[]{APK_SIG_SCHEME_V2_DIGEST_ALGORITHM});
outputChunks = ApkSignerV2.sign(v1SignedApk, signerConfigs);
// This assumes outputChunks are array-backed. To avoid this assumption, the
// code could be rewritten to use FileChannel.
for (ByteBuffer outputChunk : outputChunks) {
outputStream.write(outputChunk.array(),
outputChunk.arrayOffset() + outputChunk.position(), outputChunk.remaining());
outputChunk.position(outputChunk.limit());
}
}
/**
* Write to another stream and track how many bytes have been
* written.
*/
private static class CountOutputStream extends FilterOutputStream {
private int mCount;
public CountOutputStream(OutputStream out) {
super(out);
mCount = 0;
}
@Override
public void write(int b) throws IOException {
super.write(b);
mCount++;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
super.write(b, off, len);
mCount += len;
}
public int size() {
return mCount;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path_1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 20 8 L 17.19 8 C 16.74 7.22 16.12 6.55 15.37 6.04 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.49 5 12 5 C 11.51 5 11.04 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6.04 C 7.88 6.55 7.26 7.22 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.04 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.04 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 7.85 19.79 9.78 21 12 21 C 14.22 21 16.15 19.79 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.96 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.96 10.33 17.91 10 L 20 10 L 20 8 Z M 14 16 L 10 16 L 10 14 L 14 14 L 14 16 Z M 14 12 L 10 12 L 10 10 L 14 10 L 14 12 Z" />
</vector>
</aapt:attr>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 20 8 L 20 8 L 17.19 8 C 16.74 7.22 16.12 6.55 15.37 6.04 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.49 5 12 5 C 11.51 5 11.04 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6.04 C 7.88 6.55 7.26 7.22 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.04 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.04 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 7.85 19.79 9.78 21 12 21 C 14.22 21 16.15 19.79 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.96 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.96 10.33 17.91 10 L 20 10 L 20 8 M 14 16 C 14 15.43 14 14.859 14 14.289 L 14 14 C 13.869 14 13.739 14 13.608 14 C 12.405 14 11.203 14 10 14 C 10 14.509 10 15.017 10 15.526 C 10 15.684 10 15.842 10 16 L 10.33 16 C 10.392 16 10.454 16 10.515 16 C 11.677 16 12.838 16 14 16 C 14 16 14 16 14 16 M 14 10 L 14 12 L 14 12 L 10 12 L 10 10 L 14 10 M 12 15 L 12 15 L 12 15 L 12 15 L 12 15 L 12 15"
android:valueTo="M 20 8 L 18.595 8 L 17.19 8 C 16.74 7.2 16.12 6.5 15.37 6 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.5 5 12 5 C 11.5 5 11.05 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6 C 7.87 6.5 7.26 7.21 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.03 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.03 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 8.47 20.87 12.14 21.84 15 20.18 C 15.91 19.66 16.67 18.9 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.97 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.97 10.33 17.91 10 L 20 10 L 20 8 M 14.828 17.828 C 15.578 17.079 16 16.06 16 15 L 16 11 C 16 9.94 15.578 8.921 14.828 8.172 C 14.079 7.422 13.06 7 12 7 C 10.94 7 9.921 7.422 9.172 8.172 C 8.422 8.921 8 9.94 8 11 L 8 15 C 8 16.06 8.422 17.079 9.172 17.828 C 9.921 18.578 10.94 19 12 19 C 13.06 19 14.079 18.578 14.828 17.828 M 14 10 L 14 11 L 14 12 L 10 12 L 10 10 L 14 10 M 10 14 L 14 14 L 14 16 L 10 16 L 10 14 L 10 14"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path_1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 20 8 L 18.595 8 L 17.19 8 C 16.74 7.2 16.12 6.5 15.37 6 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.5 5 12 5 C 11.5 5 11.05 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6 C 7.87 6.5 7.26 7.21 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.03 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.03 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 8.47 20.87 12.14 21.84 15 20.18 C 15.91 19.66 16.67 18.9 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.97 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.97 10.33 17.91 10 L 20 10 L 20 8 M 14.828 17.828 C 15.578 17.079 16 16.06 16 15 L 16 11 C 16 9.94 15.578 8.921 14.828 8.172 C 14.079 7.422 13.06 7 12 7 C 10.94 7 9.921 7.422 9.172 8.172 C 8.422 8.921 8 9.94 8 11 L 8 15 C 8 16.06 8.422 17.079 9.172 17.828 C 9.921 18.578 10.94 19 12 19 C 13.06 19 14.079 18.578 14.828 17.828 M 14 10 L 14 11 L 14 12 L 10 12 L 10 10 L 14 10 M 10 14 L 14 14 L 14 16 L 10 16 L 10 14 L 10 14" />
</vector>
</aapt:attr>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 20 8 L 18.595 8 L 17.19 8 C 16.74 7.2 16.12 6.5 15.37 6 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.5 5 12 5 C 11.5 5 11.05 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6 C 7.87 6.5 7.26 7.21 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.03 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.03 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 8.47 20.87 12.14 21.84 15 20.18 C 15.91 19.66 16.67 18.9 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.97 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.97 10.33 17.91 10 L 20 10 L 20 8 M 14.828 17.828 C 15.578 17.079 16 16.06 16 15 L 16 11 C 16 9.94 15.578 8.921 14.828 8.172 C 14.079 7.422 13.06 7 12 7 C 10.94 7 9.921 7.422 9.172 8.172 C 8.422 8.921 8 9.94 8 11 L 8 15 C 8 16.06 8.422 17.079 9.172 17.828 C 9.921 18.578 10.94 19 12 19 C 13.06 19 14.079 18.578 14.828 17.828 M 14 10 L 14 11 L 14 12 L 10 12 L 10 10 L 14 10 M 10 14 L 14 14 L 14 16 L 10 16 L 10 14 L 10 14"
android:valueTo="M 20 8 L 20 8 L 17.19 8 C 16.74 7.22 16.12 6.55 15.37 6.04 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.49 5 12 5 C 11.51 5 11.04 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6.04 C 7.88 6.55 7.26 7.22 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.04 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.04 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 7.85 19.79 9.78 21 12 21 C 14.22 21 16.15 19.79 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.96 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.96 10.33 17.91 10 L 20 10 L 20 8 M 14 16 C 14 15.43 14 14.859 14 14.289 L 14 14 C 13.869 14 13.739 14 13.608 14 C 12.405 14 11.203 14 10 14 C 10 14.509 10 15.017 10 15.526 C 10 15.684 10 15.842 10 16 L 10.33 16 C 10.392 16 10.454 16 10.515 16 C 11.677 16 12.838 16 14 16 C 14 16 14 16 14 16 M 14 10 L 14 12 L 14 12 L 10 12 L 10 10 L 14 10 M 12 15 L 12 15 L 12 15 L 12 15 L 12 15 L 12 15"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,27 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#000000"
android:pathData="M 12 2 C 6.5 2 2 6.5 2 12 C 2 17.5 6.5 22 12 22 C 17.5 22 22 17.5 22 12 C 22 6.5 17.5 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 16.59 7.58 L 10 14.17 L 7.41 11.59 L 6 13 L 10 17 L 18 9 L 16.59 7.58 Z" />
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="500"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 12 2 C 9.217 2 6.689 3.152 4.872 5.004 C 3.098 6.811 2 9.283 2 12 C 2 14.744 3.12 17.24 4.927 19.052 C 6.74 20.87 9.244 22 12 22 C 13.911 22 15.701 21.457 17.224 20.517 C 18.628 19.651 19.804 18.448 20.638 17.024 C 21.503 15.545 22 13.828 22 12 C 22 10.2 21.518 8.507 20.677 7.044 C 19.755 5.441 18.402 4.114 16.779 3.224 C 15.357 2.444 13.728 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 6 13 L 10 17 L 18 9 L 16.59 7.58 L 16.59 7.58 L 10 14.17 L 7.41 11.59 L 6 13"
android:valueTo="M 12 2 C 9.349 2 6.804 3.054 4.929 4.929 C 3.054 6.804 2 9.349 2 12 C 2 14.651 3.054 17.196 4.929 19.071 C 6.804 20.946 9.349 22 12 22 C 13.755 22 15.48 21.538 17 20.66 C 18.52 19.783 19.783 18.52 20.66 17 C 21.538 15.48 22 13.755 22 12 C 22 10.245 21.538 8.52 20.66 7 C 19.783 5.48 18.52 4.217 17 3.34 C 15.48 2.462 13.755 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 7 13 L 7 13 L 17 13 L 17 11 L 17 11 L 7 11 L 7 11 L 7 11"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,27 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path_1"
android:fillColor="#000000"
android:pathData="M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 12 2 C 9.349 2 6.804 3.054 4.929 4.929 C 3.054 6.804 2 9.349 2 12 C 2 14.651 3.054 17.196 4.929 19.071 C 6.804 20.946 9.349 22 12 22 C 13.755 22 15.48 21.538 17 20.66 C 18.52 19.783 19.783 18.52 20.66 17 C 21.538 15.48 22 13.755 22 12 C 22 10.245 21.538 8.52 20.66 7 C 19.783 5.48 18.52 4.217 17 3.34 C 15.48 2.462 13.755 2 12 2 M 7 13 L 17 13 L 17 11 L 7 11" />
</vector>
</aapt:attr>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="500"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 12 2 C 9.349 2 6.804 3.054 4.929 4.929 C 3.054 6.804 2 9.349 2 12 C 2 14.651 3.054 17.196 4.929 19.071 C 6.804 20.946 9.349 22 12 22 C 13.755 22 15.48 21.538 17 20.66 C 18.52 19.783 19.783 18.52 20.66 17 C 21.538 15.48 22 13.755 22 12 C 22 10.245 21.538 8.52 20.66 7 C 19.783 5.48 18.52 4.217 17 3.34 C 15.48 2.462 13.755 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 7 13 L 7 13 L 17 13 L 17 11 L 17 11 L 7 11 L 7 11 L 7 11"
android:valueTo="M 12 2 C 9.217 2 6.689 3.152 4.872 5.004 C 3.098 6.811 2 9.283 2 12 C 2 14.856 3.213 17.442 5.149 19.268 C 6.942 20.96 9.356 22 12 22 C 14.061 22 15.982 21.368 17.578 20.288 C 19.114 19.249 20.349 17.796 21.119 16.092 C 21.685 14.841 22 13.456 22 12 C 22 10.122 21.475 8.361 20.566 6.856 C 19.691 5.408 18.46 4.197 16.997 3.347 C 15.524 2.491 13.817 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 6 13 L 10 17 L 18 9 L 16.59 7.58 L 16.59 7.58 L 10 14.17 L 7.41 11.59 L 6 13"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,387 @@
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="720dp"
android:height="720dp"
android:viewportWidth="720"
android:viewportHeight="720">
<group android:name="bottom">
<path
android:name="chin"
android:fillColor="#000000"
android:pathData="M 332.48 421.18 C 332.48 421.18 336.25 443.63 331.66 493.13 C 325.9 555.19 355.3 653.77 355.3 653.77 C 355.3 653.77 395.4 554.99 388.4 491.18 C 382.65 438.73 391 420.39 389.22 422.85 C 358.41 465.42 332.47 421.18 332.47 421.18 Z" />
<path
android:name="jawline_right"
android:fillAlpha="0"
android:fillColor="#000000"
android:pathData="M 407.6 474.45 C 412.61 513.22 407.03 534.46 399.79 575.96 C 396.13 596.95 474.57 512.86 504.65 462.73 C 509.67 454.37 475.88 495.33 442.46 466.08 C 419.28 445.8 415.3 439.64 397.28 422.02 C 391.2 416.08 404.02 446.74 407.6 474.45 Z" />
<path
android:name="jawline_left"
android:fillAlpha="0"
android:fillColor="#000000"
android:pathData="M 321.99 425.09 C 303.97 442.71 299.99 448.87 276.81 469.15 C 243.39 498.4 209.6 457.44 214.62 465.8 C 244.7 515.93 323.14 600.02 319.48 579.03 C 312.24 537.53 306.66 516.29 311.67 477.52 C 315.25 449.81 328.07 419.15 321.99 425.09 Z"
android:strokeAlpha="0" />
<path
android:name="moustache_right"
android:fillAlpha="0"
android:fillColor="#000000"
android:pathData="M 399.15 355.87 C 435.82 366.44 450.04 417.37 487.06 423.67 C 494.71 424.97 503.33 427.27 513.37 426.79 C 532.14 425.89 555.88 415.28 587.59 370.29 C 596.97 356.99 564.32 455.95 482.46 457.15 C 422.5 458.03 415.49 398.45 375.53 396.64 C 361.1 395.99 360.19 368.47 360.19 368.47 C 360.19 368.47 377.41 349.61 399.15 355.87 Z" />
<path
android:name="moustache_left"
android:fillAlpha="0"
android:fillColor="#000000"
android:pathData="M 321.51 355.59 C 284.84 366.16 270.62 417.09 233.6 423.39 C 225.95 424.69 217.33 426.99 207.29 426.51 C 188.52 425.61 164.78 415 133.07 370.01 C 123.69 356.71 156.34 455.67 238.2 456.87 C 298.16 457.75 305.17 398.17 345.13 396.36 C 359.56 395.71 360.47 368.19 360.47 368.19 C 360.47 368.19 343.25 349.33 321.51 355.59 Z" />
<path
android:name="cheek_right"
android:fillAlpha="0"
android:fillColor="#000000"
android:pathData="M 458.64 355.09 C 495.51 383.03 484.52 413.79 505.21 405.01 C 574.91 375.46 562.72 223.8 557.08 242.14 C 525.31 345.55 456.09 351.34 389.47 303.77 C 376.46 294.48 437.85 339.34 458.63 355.08 Z" />
<path
android:name="cheek_left"
android:fillAlpha="0"
android:fillColor="#000000"
android:pathData="M 330.91 303.77 C 264.29 351.33 195.07 345.55 163.3 242.14 C 157.67 223.8 145.48 375.45 215.17 405.01 C 235.87 413.79 224.87 383.03 261.74 355.09 C 282.52 339.34 343.91 294.49 330.9 303.78 Z" />
</group>
<group android:name="top">
<path
android:name="eye_right"
android:fillAlpha="0"
android:fillColor="#000000"
android:pathData="M 465.61 318 C 546.04 314.68 560.9 182.83 554.57 198.92 C 526.18 271.14 418.71 243.97 408.44 289.56 C 406.42 298.5 426.64 319.62 465.61 318.01 Z" />
<path
android:name="eye_left"
android:fillAlpha="0"
android:fillColor="#000000"
android:pathData="M 311.95 289.55 C 301.68 243.96 194.2 271.14 165.82 198.91 C 159.5 182.82 174.35 314.67 254.78 317.99 C 293.75 319.6 313.97 298.49 311.95 289.54 Z" />
<path
android:name="forehead_right"
android:fillAlpha="0"
android:fillColor="#000000"
android:pathData="M 403.42 269.47 C 403.42 269.47 447.15 245.97 484.58 235.73 C 519.57 226.15 545.8 202.6 548.72 177.72 C 550.9 159.19 521.67 124.17 521.67 124.17 C 521.67 124.17 501.16 181.07 474.26 209.51 C 444.98 240.47 456.11 236.29 403.42 269.47 Z" />
<path
android:name="forehead_left"
android:fillAlpha="0"
android:fillColor="#000000"
android:pathData="M 246.13 209.51 C 219.23 181.07 198.72 124.17 198.72 124.17 C 198.72 124.17 169.49 159.18 171.67 177.72 C 174.6 202.6 200.83 226.15 235.81 235.73 C 273.24 245.98 316.97 269.47 316.97 269.47 C 264.28 236.29 275.42 240.47 246.13 209.51 Z" />
<path
android:name="forehead_top_right"
android:fillAlpha="0"
android:fillColor="#000000"
android:pathData="M 398.12 265.85 C 445.48 227 470.65 176.31 511.63 120.83 C 519.36 110.37 477.05 85.13 460.32 83.46 C 443.59 81.79 429.55 143.25 427.97 179.4 C 426.53 212.41 391.76 271.08 398.13 265.85 Z" />
<path
android:name="forehead_top_left"
android:fillAlpha="0"
android:fillColor="#000000"
android:pathData="M 292.42 179.39 C 290.84 143.24 276.8 81.78 260.07 83.45 C 243.34 85.12 201.03 110.36 208.76 120.82 C 249.74 176.3 274.9 226.99 322.27 265.84 C 328.64 271.06 293.87 212.39 292.43 179.39 Z" />
<path
android:name="forehead_center_right"
android:fillColor="#000000"
android:pathData="M 402.86 140.35 C 406.2 113.59 418.23 94.03 442.18 77.6 C 421.01 70.52 403.41 64.77 394.21 72.3 C 385.01 79.83 360.01 105 363.36 145.98 C 366.7 186.96 363.54 340.07 370.79 337.23 C 374.69 232.36 407.88 202.23 402.86 140.34 Z" />
<path
android:name="forehead_center_left"
android:fillColor="#000000"
android:pathData="M 349.59 337.24 C 356.83 340.07 353.67 186.97 357.02 145.99 C 360.36 105.01 335.37 79.83 326.17 72.31 C 316.97 64.78 299.37 70.53 278.2 77.61 C 302.15 94.04 314.18 113.59 317.52 140.36 C 312.5 202.25 345.69 232.38 349.59 337.25 Z" />
</group>
</vector>
</aapt:attr>
<target android:name="jawline_right">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:startOffset="400"
android:valueFrom="M 332.48 421.18 C 332.48 421.18 336.25 443.63 331.66 493.13 C 325.9 555.19 355.3 653.77 355.3 653.77 C 355.3 653.77 395.4 554.99 388.4 491.18 C 382.65 438.73 391 420.39 389.22 422.85 C 358.41 465.42 332.47 421.18 332.47 421.18 Z"
android:valueTo="M 407.6 474.45 C 412.61 513.22 407.03 534.46 399.79 575.96 C 396.13 596.95 474.57 512.86 504.65 462.73 C 509.67 454.37 475.88 495.33 442.46 466.08 C 419.28 445.8 415.3 439.64 397.28 422.02 C 391.2 416.08 404.02 446.74 407.6 474.45 Z"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="400"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="jawline_left">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:startOffset="400"
android:valueFrom="M 332.48 421.18 C 332.48 421.18 336.25 443.63 331.66 493.13 C 325.9 555.19 355.3 653.77 355.3 653.77 C 355.3 653.77 395.4 554.99 388.4 491.18 C 382.65 438.73 391 420.39 389.22 422.85 C 358.41 465.42 332.47 421.18 332.47 421.18 Z"
android:valueTo="M 321.99 425.09 C 303.97 442.71 299.99 448.87 276.81 469.15 C 243.39 498.4 209.6 457.44 214.62 465.8 C 244.7 515.93 323.14 600.02 319.48 579.03 C 312.24 537.53 306.66 516.29 311.67 477.52 C 315.25 449.81 328.07 419.15 321.99 425.09 Z"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="400"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="forehead_center_right">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="400"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 465 120 C 458 113 451 106 444 99 C 438 93 432 90 423 90 C 381 90 339 90 297 90 C 288 90 282 93 276 99 C 269 106 262 113 255 120 C 230 120 205 120 180 120 C 162 120 150 135 150 150 C 150 165 162 180 180 180 C 300 180 420 180 540 180 C 555 180 570 168 570 150 C 570 132 555 120 540 120 C 540 120 540 120 540 120 L 465 120"
android:valueTo="M 402.86 140.35 C 406.2 113.59 418.23 94.03 442.18 77.6 C 421.01 70.52 403.41 64.77 394.21 72.3 C 385.01 79.83 360.01 105 363.36 145.98 C 363.917 152.81 364.293 162.755 364.567 174.573 C 364.84 186.391 365.012 200.083 365.16 214.408 C 365.307 228.732 365.431 243.689 365.609 258.038 C 365.787 272.386 366.019 286.126 366.384 298.016 C 366.749 309.906 367.246 319.946 367.954 326.895 C 368.663 333.845 369.582 337.703 370.79 337.23 C 372.09 302.273 376.644 275.621 381.953 253.329 C 387.262 231.037 393.326 213.104 397.643 195.588 C 401.961 178.071 404.533 160.97 402.86 140.34 L 402.86 140.35"
android:valueType="pathType" />
</aapt:attr>
</target>
<target android:name="moustache_right">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:startOffset="600"
android:valueFrom="M 397.28 422.02 C 391.2 416.08 404.02 446.74 407.6 474.45 C 407.6 474.45 407.6 474.45 407.6 474.45 C 410.105 493.835 409.962 508.837 408.289 524.181 C 406.615 539.525 403.41 555.21 399.79 575.96 C 396.13 596.95 474.57 512.86 504.65 462.73 C 507.16 458.55 499.967 466.7 487.97 472.239 C 475.973 477.777 459.17 480.705 442.46 466.08 C 419.28 445.8 415.3 439.64 397.28 422.02"
android:valueTo="M 399.15 355.87 C 377.41 349.61 360.19 368.47 360.19 368.47 C 360.19 368.47 361.1 395.99 375.53 396.64 C 415.49 398.45 422.5 458.03 482.46 457.15 C 564.32 455.95 596.97 356.99 587.59 370.29 C 555.88 415.28 532.14 425.89 513.37 426.79 C 503.33 427.27 494.71 424.97 487.06 423.67 C 450.04 417.37 435.82 366.44 399.15 355.87 C 399.15 355.87 399.15 355.87 399.15 355.87"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="600"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="moustache_left">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:startOffset="600"
android:valueFrom="M 310.981 527.251 C 309.308 511.908 309.165 496.905 311.67 477.52 C 315.25 449.81 328.07 419.15 321.99 425.09 L 321.99 425.09 C 303.97 442.71 299.99 448.87 276.81 469.15 C 260.1 483.775 243.298 480.848 231.3 475.309 C 219.303 469.77 212.11 461.62 214.62 465.8 C 244.7 515.93 323.14 600.02 319.48 579.03 C 315.86 558.28 312.655 542.595 310.981 527.251"
android:valueTo="M 345.13 396.36 C 359.56 395.71 360.47 368.19 360.47 368.19 C 360.47 368.19 343.25 349.33 321.51 355.59 L 321.51 355.59 C 284.84 366.16 270.62 417.09 233.6 423.39 C 225.95 424.69 217.33 426.99 207.29 426.51 C 188.52 425.61 164.78 415 133.07 370.01 C 123.69 356.71 156.34 455.67 238.2 456.87 C 298.16 457.75 305.17 398.17 345.13 396.36"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="600"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="forehead_center_left">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="400"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 465 120 C 458 113 451 106 444 99 C 438 93 432 90 423 90 C 381 90 339 90 297 90 C 288 90 282 93 276 99 C 269 106 262 113 255 120 C 230 120 205 120 180 120 C 162 120 150 135 150 150 C 150 165 162 180 180 180 C 300 180 420 180 540 180 C 555 180 570 168 570 150 C 570 132 555 120 540 120 C 540 120 540 120 540 120 L 465 120"
android:valueTo="M 349.59 337.24 C 353.21 338.655 354.23 301.087 354.764 258.044 C 355.298 215 355.345 166.48 357.02 145.99 C 360.36 105.01 335.37 79.83 326.17 72.31 C 316.97 64.78 299.37 70.53 278.2 77.61 C 302.15 94.04 314.18 113.59 317.52 140.36 C 315.847 160.99 318.419 178.091 322.737 195.608 C 327.054 213.124 333.118 231.057 338.427 253.349 C 343.736 275.641 348.29 302.293 349.59 337.25 C 349.59 337.249 349.59 337.249 349.59 337.248 C 349.59 337.247 349.59 337.247 349.59 337.246 C 349.59 337.245 349.59 337.245 349.59 337.244 C 349.59 337.243 349.59 337.243 349.59 337.242 L 349.59 337.24"
android:valueType="pathType" />
</aapt:attr>
</target>
<target android:name="cheek_right">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:startOffset="800"
android:valueFrom="M 375.53 396.64 C 361.1 395.99 360.19 368.47 360.19 368.47 C 360.19 368.47 377.41 349.61 399.15 355.87 C 399.15 355.87 399.15 355.87 399.15 355.87 C 435.82 366.44 450.04 417.37 487.06 423.67 C 494.71 424.97 503.33 427.27 513.37 426.79 C 532.14 425.89 555.88 415.28 587.59 370.29 C 596.97 356.99 564.32 455.95 482.46 457.15 C 422.5 458.03 415.49 398.45 375.53 396.64"
android:valueTo="M 458.64 355.09 C 458.637 355.087 458.633 355.083 458.63 355.08 C 437.85 339.34 376.46 294.48 389.47 303.77 C 411.677 319.627 434.172 329.554 455.473 331.914 C 476.774 334.274 496.881 329.065 514.31 314.649 C 531.739 300.233 546.49 276.61 557.08 242.14 C 558.538 237.398 560.435 244.022 561.24 256.964 C 563.549 294.069 556.887 383.101 505.21 405.01 C 484.52 413.79 495.51 383.03 458.64 355.09"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="800"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="cheek_left">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:startOffset="800"
android:valueFrom="M 321.51 355.59 C 343.25 349.33 360.47 368.19 360.47 368.19 C 360.47 368.19 359.56 395.71 345.13 396.36 C 305.17 398.17 298.16 457.75 238.2 456.87 C 156.34 455.67 123.69 356.71 133.07 370.01 C 164.78 415 188.52 425.61 207.29 426.51 C 217.33 426.99 225.95 424.69 233.6 423.39 C 270.62 417.09 284.84 366.16 321.51 355.59 C 321.51 355.59 321.51 355.59 321.51 355.59"
android:valueTo="M 330.91 303.77 C 330.907 303.773 330.903 303.777 330.9 303.78 C 343.91 294.49 282.52 339.34 261.74 355.09 C 224.87 383.03 235.87 413.79 215.17 405.01 C 145.48 375.45 157.67 223.8 163.3 242.14 C 171.243 267.993 181.526 287.743 193.524 302.084 C 205.522 316.425 219.234 325.356 234.036 329.569 C 248.838 333.782 264.729 333.277 281.083 328.747 C 297.438 324.216 314.255 315.66 330.91 303.77"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="800"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="chin">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="400"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 180 270 C 180 370 180 470 180 570 C 180 570 180 570 180 570 C 180 603 207 630 240 630 C 320 630 400 630 480 630 C 513 630 540 603 540 570 C 540 470 540 370 540 270 C 540 237 513 210 480 210 C 400 210 320 210 240 210 C 207 210 180 237 180 270"
android:valueTo="M 332.48 421.18 C 332.48 421.18 336.25 443.63 331.66 493.13 C 330.031 510.682 331.214 531.156 333.75 551.494 C 340.179 603.067 355.3 653.77 355.3 653.77 C 355.3 653.77 374.498 606.479 384.008 556.202 C 388.243 533.816 390.557 510.839 388.4 491.18 C 387.14 479.683 386.557 469.826 386.396 461.466 C 385.824 431.689 390.61 420.929 389.22 422.85 C 358.41 465.42 332.47 421.18 332.47 421.18 C 332.473 421.18 332.477 421.18 332.48 421.18"
android:valueType="pathType" />
</aapt:attr>
</target>
<target android:name="forehead_top_left">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:startOffset="400"
android:valueFrom="M 357.02 145.99 C 355.345 166.48 355.298 215 354.764 258.044 C 354.23 301.087 353.21 338.655 349.59 337.24 C 349.59 337.241 349.59 337.241 349.59 337.242 C 349.59 337.243 349.59 337.243 349.59 337.244 C 349.59 337.245 349.59 337.245 349.59 337.246 C 349.59 337.247 349.59 337.247 349.59 337.248 C 349.59 337.249 349.59 337.249 349.59 337.25 C 348.29 302.293 343.736 275.641 338.427 253.349 C 333.118 231.057 327.054 213.124 322.737 195.608 C 318.419 178.091 315.847 160.99 317.52 140.36 C 314.18 113.59 302.15 94.04 278.2 77.61 C 299.37 70.53 316.97 64.78 326.17 72.31 C 335.37 79.83 360.36 105.01 357.02 145.99"
android:valueTo="M 292.42 179.39 C 292.423 179.39 292.427 179.39 292.43 179.39 C 293.87 212.39 328.64 271.06 322.27 265.84 C 317.007 261.523 312.018 257.06 307.25 252.461 C 302.483 247.862 297.938 243.126 293.563 238.263 C 289.188 233.401 284.982 228.411 280.895 223.304 C 276.807 218.198 272.837 212.974 268.933 207.643 C 265.028 202.311 261.189 196.873 257.364 191.336 C 253.538 185.799 249.726 180.165 245.875 174.442 C 242.024 168.72 238.134 162.909 234.154 157.02 C 230.173 151.13 226.101 145.163 221.886 139.126 C 217.671 133.09 213.313 126.984 208.76 120.82 C 201.03 110.36 243.34 85.12 260.07 83.45 C 276.8 81.78 290.84 143.24 292.42 179.39"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="400"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="forehead_top_right">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:startOffset="400"
android:valueFrom="M 367.954 326.895 C 368.663 333.845 369.582 337.703 370.79 337.23 C 372.09 302.273 376.644 275.621 381.953 253.329 C 387.262 231.037 393.326 213.104 397.643 195.588 C 401.961 178.071 404.533 160.97 402.86 140.34 C 402.86 140.343 402.86 140.347 402.86 140.35 C 406.2 113.59 418.23 94.03 442.18 77.6 C 421.01 70.52 403.41 64.77 394.21 72.3 C 385.01 79.83 360.01 105 363.36 145.98 C 363.917 152.81 364.293 162.755 364.567 174.573 C 364.84 186.391 365.012 200.083 365.16 214.408 C 365.307 228.732 365.431 243.689 365.609 258.038 C 365.787 272.386 366.019 286.126 366.384 298.016 C 366.749 309.906 367.246 319.946 367.954 326.895"
android:valueTo="M 398.12 265.85 C 405.87 259.493 413.025 252.818 419.753 245.858 C 428.143 237.179 435.867 228.055 443.249 218.546 C 448.95 211.202 454.447 203.629 459.889 195.854 C 466.098 186.982 472.234 177.847 478.52 168.49 C 485.053 158.766 491.746 148.801 498.849 138.642 C 502.956 132.767 507.201 126.826 511.63 120.83 C 519.36 110.37 477.05 85.13 460.32 83.46 C 443.59 81.79 429.55 143.25 427.97 179.4 C 426.53 212.41 391.76 271.08 398.13 265.85 C 398.129 265.85 398.128 265.85 398.127 265.85 C 398.127 265.85 398.126 265.85 398.125 265.85 C 398.124 265.85 398.123 265.85 398.123 265.85 C 398.122 265.85 398.121 265.85 398.12 265.85"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="400"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="forehead_left">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:startOffset="600"
android:valueFrom="M 292.42 179.39 C 291.51 158.576 286.47 129.373 279.164 108.658 C 273.781 93.395 267.168 82.742 260.07 83.45 C 243.34 85.12 201.03 110.36 208.76 120.82 C 227.553 146.262 243.019 170.697 258.825 193.445 C 277.487 220.301 296.623 244.806 322.27 265.84 C 328.64 271.06 293.87 212.39 292.43 179.39 Z"
android:valueTo="M 246.13 209.51 C 232.68 195.29 220.827 173.955 212.338 156.175 C 203.847 138.395 198.72 124.17 198.72 124.17 C 198.72 124.17 169.49 159.18 171.67 177.72 C 174.6 202.6 200.83 226.15 235.81 235.73 C 273.24 245.98 316.97 269.47 316.97 269.47 C 264.28 236.29 275.42 240.47 246.13 209.51 Z"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="600"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="forehead_right">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:startOffset="600"
android:valueFrom="M 398.13 265.85 L 398.12 265.85 C 421.8 246.425 439.933 224.04 457.267 199.576 C 474.603 175.113 491.14 148.57 511.63 120.83 C 519.36 110.37 477.05 85.13 460.32 83.46 C 443.59 81.79 429.55 143.25 427.97 179.4 C 426.53 212.41 391.76 271.08 398.13 265.85"
android:valueTo="M 403.42 269.47 L 403.42 269.47 C 403.42 269.47 447.15 245.97 484.58 235.73 C 519.57 226.15 545.8 202.6 548.72 177.72 C 550.9 159.19 521.67 124.17 521.67 124.17 C 521.67 124.17 501.16 181.07 474.26 209.51 C 444.98 240.47 456.11 236.29 403.42 269.47"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="600"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="eye_right">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:startOffset="800"
android:valueFrom="M 484.58 235.73 C 519.57 226.15 545.8 202.6 548.72 177.72 C 550.9 159.19 521.67 124.17 521.67 124.17 C 521.67 124.17 501.16 181.07 474.26 209.51 C 444.98 240.47 456.11 236.29 403.42 269.47 C 403.42 269.47 403.42 269.47 403.42 269.47 C 403.42 269.47 447.15 245.97 484.58 235.73"
android:valueTo="M 465.61 318 C 546.04 314.68 560.9 182.83 554.57 198.92 C 545.107 222.993 526.857 236.023 506.349 244.386 C 485.841 252.749 463.076 256.444 444.581 261.847 C 426.087 267.251 411.863 274.363 408.44 289.56 C 406.42 298.5 426.64 319.62 465.61 318.01 C 465.61 318.007 465.61 318.003 465.61 318"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="800"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="eye_left">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:startOffset="800"
android:valueFrom="M 246.13 209.51 L 246.13 209.51 C 275.42 240.47 264.28 236.29 316.97 269.47 C 316.97 269.47 273.24 245.98 235.81 235.73 C 200.83 226.15 174.6 202.6 171.67 177.72 C 169.49 159.18 198.72 124.17 198.72 124.17 C 198.72 124.17 203.847 138.395 212.338 156.175 C 220.827 173.955 232.68 195.29 246.13 209.51"
android:valueTo="M 311.95 289.55 L 311.95 289.54 C 313.97 298.49 293.75 319.6 254.78 317.99 C 227.97 316.883 208.447 301.496 194.565 282.066 C 180.684 262.637 172.444 239.166 168.201 221.894 C 163.959 204.621 163.713 193.547 165.82 198.91 C 180.01 235.025 213.975 246.287 245.676 254.22 C 277.377 262.152 306.815 266.755 311.95 289.55"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="800"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,29 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path_3"
android:fillColor="#000"
android:fillType="evenOdd"
android:pathData="M 12 3 L 20 9 L 20 21 L 15 21 L 15 14 L 9 14 L 9 21 L 4 21 L 4 9 L 12 3 Z"
android:strokeWidth="1" />
</vector>
</aapt:attr>
<target android:name="path_3">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 9 14 L 9 21 L 4 21 L 4 9 L 12 3 L 12 3 L 20 9 L 20 21 L 15 21 L 15 14 L 9 14 M 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4"
android:valueTo="M 9 13 L 9 19 L 6 19 L 6 10 L 12 5.5 L 15 7.75 L 18 10 L 18 19 L 15 19 L 15 13 L 9 13 M 4 21 L 4 9 L 12 3 L 20 9 L 20 21 L 4 21 L 4 21"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,29 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path_3"
android:fillColor="#000"
android:fillType="evenOdd"
android:pathData="M 9 13 L 9 19 L 6 19 L 6 10 L 12 5.5 L 15 7.75 L 18 10 L 18 19 L 15 19 L 15 13 L 9 13 M 4 21 L 4 9 L 12 3 L 20 9 L 20 21 L 4 21 L 4 21"
android:strokeWidth="1" />
</vector>
</aapt:attr>
<target android:name="path_3">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 9 13 L 9 19 L 6 19 L 6 10 L 12 5.5 L 15 7.75 L 18 10 L 18 19 L 15 19 L 15 13 L 9 13 M 4 21 L 4 9 L 12 3 L 20 9 L 20 21 L 4 21 L 4 21"
android:valueTo="M 9 14 L 9 21 L 4 21 L 4 9 L 12 3 L 12 3 L 20 9 L 20 21 L 15 21 L 15 14 L 9 14 M 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,428 @@
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="32dp"
android:height="32dp"
android:viewportWidth="720"
android:viewportHeight="720">
<group android:name="bin_group">
<path
android:name="path_1"
android:fillColor="#000000"
android:pathData="M 407.6 474.45 C 412.61 513.22 407.03 534.46 399.79 575.96 C 396.13 596.95 474.57 512.86 504.65 462.73 C 509.67 454.37 475.88 495.33 442.46 466.08 C 419.28 445.8 415.3 439.64 397.28 422.02 C 391.2 416.08 404.02 446.74 407.6 474.45 Z" />
<path
android:name="path_2"
android:fillColor="#000000"
android:pathData="M 321.99 425.09 C 303.97 442.71 299.99 448.87 276.81 469.15 C 243.39 498.4 209.6 457.44 214.62 465.8 C 244.7 515.93 323.14 600.02 319.48 579.03 C 312.24 537.53 306.66 516.29 311.67 477.52 C 315.25 449.81 328.07 419.15 321.99 425.09 Z" />
<path
android:name="bin"
android:fillColor="#000000"
android:pathData="M 332.48 421.18 C 332.48 421.18 336.25 443.63 331.66 493.13 C 325.9 555.19 355.3 653.77 355.3 653.77 C 355.3 653.77 395.4 554.99 388.4 491.18 C 382.65 438.73 391 420.39 389.22 422.85 C 358.41 465.42 332.47 421.18 332.47 421.18 Z" />
</group>
<group android:name="trash_group">
<path
android:name="path_6"
android:fillColor="#000000"
android:pathData="M 330.91 303.77 C 264.29 351.33 195.07 345.55 163.3 242.14 C 157.67 223.8 145.48 375.45 215.17 405.01 C 235.87 413.79 224.87 383.03 261.74 355.09 C 282.52 339.34 343.91 294.49 330.9 303.78 Z" />
<path
android:name="path_7"
android:fillColor="#000000"
android:pathData="M 465.61 318 C 546.04 314.68 560.9 182.83 554.57 198.92 C 526.18 271.14 418.71 243.97 408.44 289.56 C 406.42 298.5 426.64 319.62 465.61 318.01 Z" />
<path
android:name="path_8"
android:fillColor="#000000"
android:pathData="M 311.95 289.55 C 301.68 243.96 194.2 271.14 165.82 198.91 C 159.5 182.82 174.35 314.67 254.78 317.99 C 293.75 319.6 313.97 298.49 311.95 289.54 Z" />
<path
android:name="path_9"
android:fillColor="#000000"
android:pathData="M 403.42 269.47 C 403.42 269.47 447.15 245.97 484.58 235.73 C 519.57 226.15 545.8 202.6 548.72 177.72 C 550.9 159.19 521.67 124.17 521.67 124.17 C 521.67 124.17 501.16 181.07 474.26 209.51 C 444.98 240.47 456.11 236.29 403.42 269.47 Z" />
<path
android:name="path_10"
android:fillColor="#000000"
android:pathData="M 246.13 209.51 C 219.23 181.07 198.72 124.17 198.72 124.17 C 198.72 124.17 169.49 159.18 171.67 177.72 C 174.6 202.6 200.83 226.15 235.81 235.73 C 273.24 245.98 316.97 269.47 316.97 269.47 C 264.28 236.29 275.42 240.47 246.13 209.51 Z" />
<path
android:name="path_11"
android:fillColor="#000000"
android:pathData="M 398.12 265.85 C 445.48 227 470.65 176.31 511.63 120.83 C 519.36 110.37 477.05 85.13 460.32 83.46 C 443.59 81.79 429.55 143.25 427.97 179.4 C 426.53 212.41 391.76 271.08 398.13 265.85 Z" />
<path
android:name="path_12"
android:fillColor="#000000"
android:pathData="M 292.42 179.39 C 290.84 143.24 276.8 81.78 260.07 83.45 C 243.34 85.12 201.03 110.36 208.76 120.82 C 249.74 176.3 274.9 226.99 322.27 265.84 C 328.64 271.06 293.87 212.39 292.43 179.39 Z" />
<path
android:name="path_13"
android:fillColor="#000000"
android:pathData="M 402.86 140.35 C 406.2 113.59 418.23 94.03 442.18 77.6 C 421.01 70.52 403.41 64.77 394.21 72.3 C 385.01 79.83 360.01 105 363.36 145.98 C 366.7 186.96 363.54 340.07 370.79 337.23 C 374.69 232.36 407.88 202.23 402.86 140.34 Z" />
<path
android:name="path_14"
android:fillColor="#000000"
android:pathData="M 349.59 337.24 C 356.83 340.07 353.67 186.97 357.02 145.99 C 360.36 105.01 335.37 79.83 326.17 72.31 C 316.97 64.78 299.37 70.53 278.2 77.61 C 302.15 94.04 314.18 113.59 317.52 140.36 C 312.5 202.25 345.69 232.38 349.59 337.25 Z" />
<path
android:name="particle"
android:fillColor="#000000"
android:pathData="M 458.64 355.09 C 495.51 383.03 484.52 413.79 505.21 405.01 C 574.91 375.46 562.72 223.8 557.08 242.14 C 525.31 345.55 456.09 351.34 389.47 303.77 C 376.46 294.48 437.85 339.34 458.63 355.08 Z" />
</group>
<group android:name="lid_group">
<path
android:name="path_3"
android:fillColor="#000000"
android:pathData="M 399.15 355.87 C 435.82 366.44 450.04 417.37 487.06 423.67 C 494.71 424.97 503.33 427.27 513.37 426.79 C 532.14 425.89 555.88 415.28 587.59 370.29 C 596.97 356.99 564.32 455.95 482.46 457.15 C 422.5 458.03 415.49 398.45 375.53 396.64 C 361.1 395.99 360.19 368.47 360.19 368.47 C 360.19 368.47 377.41 349.61 399.15 355.87 Z" />
<path
android:name="lid"
android:fillColor="#000000"
android:pathData="M 321.51 355.59 C 284.84 366.16 270.62 417.09 233.6 423.39 C 225.95 424.69 217.33 426.99 207.29 426.51 C 188.52 425.61 164.78 415 133.07 370.01 C 123.69 356.71 156.34 455.67 238.2 456.87 C 298.16 457.75 305.17 398.17 345.13 396.36 C 359.56 395.71 360.47 368.19 360.47 368.19 C 360.47 368.19 343.25 349.33 321.51 355.59 Z" />
</group>
<group android:name="group" />
</vector>
</aapt:attr>
<target android:name="bin">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="600"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 333.031 545.392 C 335.812 570.52 340.88 595.908 345.48 615.79 C 350.691 638.314 355.3 653.77 355.3 653.77 C 355.3 653.77 361.365 638.831 368.365 616.819 C 374.111 598.753 380.487 575.921 384.658 552.675 C 388.473 531.415 390.443 509.807 388.4 491.18 C 387.5 482.968 386.945 475.592 386.643 469.002 C 385.016 433.497 390.721 420.775 389.22 422.85 C 379.853 435.793 370.935 440.711 363.06 441.288 C 345.034 442.608 332.47 421.18 332.47 421.18 C 332.473 421.18 332.477 421.18 332.48 421.18 C 332.48 421.18 334.295 431.991 333.873 455.15 C 333.687 465.364 333.065 477.98 331.66 493.13 C 330.194 508.927 331.006 527.09 333.031 545.392 M 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018"
android:valueTo="M 180 570 C 180 570 180 570 180 570 C 180 603 207 630 240 630 C 320 630 400 630 480 630 C 513 630 540 603 540 570 C 540 470 540 370 540 270 C 540 261.75 538.313 253.875 535.266 246.703 C 532.219 239.531 527.813 233.063 522.375 227.625 C 516.938 222.188 510.469 217.781 503.297 214.734 C 496.125 211.688 488.25 210 480 210 C 400 210 320 210 240 210 C 224.5 210 210.324 215.957 199.647 225.694 C 187.593 236.687 180 252.5 180 270 C 180 370 180 470 180 570 M 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018"
android:valueType="pathType" />
</aapt:attr>
</target>
<target android:name="path_1">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="600"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 399.79 575.96 C 397.28 590.356 433.39 555.323 466.029 515.717 C 480.979 497.577 495.201 478.478 504.65 462.73 C 509.67 454.37 475.88 495.33 442.46 466.08 C 433.235 458.009 427.051 452.174 421.812 446.964 C 413.888 439.082 408.128 432.628 397.28 422.02 C 396.52 421.277 396.055 421.107 395.831 421.431 C 395.607 421.755 395.623 422.574 395.824 423.81 C 396.025 425.046 396.412 426.699 396.929 428.692 C 399.516 438.66 405.363 457.131 407.6 474.45 C 407.6 474.45 407.6 474.45 407.6 474.45 C 409.194 486.786 409.716 497.347 409.454 507.351 C 409.15 518.911 407.799 529.726 405.845 541.676 C 404.175 551.886 402.064 562.923 399.79 575.96 M 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018"
android:valueTo="M 240 630 C 320 630 400 630 480 630 C 513 630 540 603 540 570 C 540 470 540 370 540 270 C 540 261.75 538.313 253.875 535.266 246.703 C 532.219 239.531 527.813 233.063 522.375 227.625 C 516.938 222.188 510.469 217.781 503.297 214.734 C 496.125 211.688 488.25 210 480 210 C 400 210 320 210 240 210 C 224.5 210 210.324 215.957 199.647 225.694 C 187.593 236.687 180 252.5 180 270 C 180 370 180 470 180 570 C 180 570 180 570 180 570 C 180 603 207 630 240 630 M 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="590"
android:valueFrom="1"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_2">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="600"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 321.99 425.09 C 303.97 442.71 299.99 448.87 276.81 469.15 C 264.286 480.111 251.71 481.213 241.144 478.745 C 223.515 474.627 211.481 460.573 214.62 465.8 C 226.834 486.156 247.022 512.111 266.45 534.357 C 294.866 566.895 321.654 591.497 319.48 579.03 C 312.24 537.53 306.66 516.29 311.67 477.52 C 312.22 473.266 312.987 468.942 313.871 464.691 C 314.766 460.385 315.78 456.154 316.807 452.148 C 317.848 448.089 318.902 444.261 319.859 440.818 C 322.231 432.286 324.01 426.118 323.521 424.67 C 323.341 424.14 322.859 424.241 321.99 425.09 C 321.99 425.09 321.99 425.09 321.99 425.09 C 321.99 425.09 321.99 425.09 321.99 425.09 M 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018"
android:valueTo="M 480 210 C 400 210 320 210 240 210 C 224.5 210 210.324 215.957 199.647 225.694 C 187.593 236.687 180 252.5 180 270 C 180 370 180 470 180 570 C 180 570 180 570 180 570 C 180 603 207 630 240 630 C 320 630 400 630 480 630 C 513 630 540 603 540 570 C 540 470 540 370 540 270 C 540 261.75 538.313 253.875 535.266 246.703 C 532.219 239.531 527.813 233.063 522.375 227.625 C 516.938 222.188 510.469 217.781 503.297 214.734 C 496.125 211.688 488.25 210 480 210 M 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018 L 324.756 381.018"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="590"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="particle">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="250"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="pathData"
android:valueFrom="M 389.47 303.77 C 376.46 294.48 437.85 339.34 458.63 355.08 C 458.633 355.083 458.637 355.087 458.64 355.09 C 495.51 383.03 484.52 413.79 505.21 405.01 C 574.91 375.46 562.72 223.8 557.08 242.14 C 525.31 345.55 456.09 351.34 389.47 303.77"
android:valueTo="M 360 120 C 342 120 330 132 330 150 C 330 168 342 180 360 180 C 378 180 390 168 390 150 C 390 132 378 120 360 120 C 360 120 360 120 360 120"
android:valueType="pathType" />
</aapt:attr>
</target>
<target android:name="path_6">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="250"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="pathData"
android:valueFrom="M 330.91 303.77 C 264.29 351.33 195.07 345.55 163.3 242.14 C 157.67 223.8 145.48 375.45 215.17 405.01 C 235.87 413.79 224.87 383.03 261.74 355.09 C 282.52 339.34 343.91 294.49 330.9 303.78 Z"
android:valueTo="M 360 120 C 342 120 330 132 330 150 C 330 168 342 180 360 180 C 378 180 390 168 390 150 C 390 132 378 120 360 120 Z"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="240"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_7">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="250"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="pathData"
android:valueFrom="M 408.44 289.56 C 406.42 298.5 426.64 319.62 465.61 318.01 C 465.61 318.008 465.61 318.007 465.61 318.005 C 465.61 318.003 465.61 318.002 465.61 318 C 546.04 314.68 560.9 182.83 554.57 198.92 C 526.18 271.14 418.71 243.97 408.44 289.56"
android:valueTo="M 360 120 C 360 120 360 120 360 120 C 342 120 330 132 330 150 C 330 168 342 180 360 180 C 378 180 390 168 390 150 C 390 132 378 120 360 120"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="240"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_8">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="250"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="pathData"
android:valueFrom="M 311.95 289.54 C 311.95 289.543 311.95 289.547 311.95 289.55 C 301.68 243.96 194.2 271.14 165.82 198.91 C 162.66 190.865 164.792 219.805 177.769 251.171 C 190.745 282.538 214.565 316.33 254.78 317.99 C 293.75 319.6 313.97 298.49 311.95 289.54"
android:valueTo="M 360 180 C 378 180 390 168 390 150 C 390 132 378 120 360 120 C 360 120 360 120 360 120 C 342 120 330 132 330 150 C 330 168 342 180 360 180"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="240"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_9">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="250"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="pathData"
android:valueFrom="M 474.26 209.51 C 444.98 240.47 456.11 236.29 403.42 269.47 C 403.42 269.47 403.42 269.47 403.42 269.47 C 403.42 269.47 447.15 245.97 484.58 235.73 C 519.57 226.15 545.8 202.6 548.72 177.72 C 550.9 159.19 521.67 124.17 521.67 124.17 C 521.67 124.17 501.16 181.07 474.26 209.51"
android:valueTo="M 360 120 C 342 120 330 132 330 150 C 330 168 342 180 360 180 C 378 180 390 168 390 150 C 390 141 387 133.5 381.75 128.25 C 376.5 123 369 120 360 120 C 360 120 360 120 360 120"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="240"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_10">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="250"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="pathData"
android:valueFrom="M 198.72 124.17 C 198.72 124.17 219.23 181.07 246.13 209.51 C 246.13 209.51 246.13 209.51 246.13 209.51 C 275.42 240.47 264.28 236.29 316.97 269.47 C 316.97 269.47 273.24 245.98 235.81 235.73 C 200.83 226.15 174.6 202.6 171.67 177.72 C 169.49 159.18 198.72 124.17 198.72 124.17"
android:valueTo="M 360 120 C 360 120 360 120 360 120 C 369 120 376.5 123 381.75 128.25 C 387 133.5 390 141 390 150 C 390 168 378 180 360 180 C 342 180 330 168 330 150 C 330 132 342 120 360 120"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="240"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_14">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="250"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="pathData"
android:valueFrom="M 326.17 72.31 C 335.37 79.83 360.36 105.01 357.02 145.99 C 353.67 186.97 356.83 340.07 349.59 337.24 C 349.59 337.243 349.59 337.247 349.59 337.25 C 345.69 232.38 312.5 202.25 317.52 140.36 C 314.18 113.59 302.15 94.04 278.2 77.61 C 288.785 74.07 298.478 70.862 306.674 69.481 C 314.87 68.1 321.57 68.545 326.17 72.31"
android:valueTo="M 360 120 C 360 120 360 120 360 120 C 378 120 390 132 390 150 C 390 168 378 180 360 180 C 342 180 330 168 330 150 C 330 144 331.333 138.667 333.778 134.222 C 336.222 129.778 339.778 126.222 344.222 123.778 C 348.667 121.333 354 120 360 120"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="240"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_13">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="250"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="pathData"
android:valueFrom="M 394.21 72.3 C 385.01 79.83 360.01 105 363.36 145.98 C 366.7 186.96 363.54 340.07 370.79 337.23 C 374.69 232.36 407.88 202.23 402.86 140.34 C 402.86 140.343 402.86 140.347 402.86 140.35 C 406.2 113.59 418.23 94.03 442.18 77.6 C 421.01 70.52 403.41 64.77 394.21 72.3"
android:valueTo="M 360 120 C 342 120 330 132 330 150 C 330 168 342 180 360 180 C 378 180 390 168 390 150 C 390 141 387 133.5 381.75 128.25 C 376.5 123 369 120 360 120 C 360 120 360 120 360 120"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="240"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_12">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="250"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="pathData"
android:valueFrom="M 292.42 179.39 C 290.84 143.24 276.8 81.78 260.07 83.45 C 243.34 85.12 201.03 110.36 208.76 120.82 C 249.74 176.3 274.9 226.99 322.27 265.84 C 328.64 271.06 293.87 212.39 292.43 179.39 Z"
android:valueTo="M 360 120 C 342 120 330 132 330 150 C 330 168 342 180 360 180 C 378 180 390 168 390 150 C 390 132 378 120 360 120 Z"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="240"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_11">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="250"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="pathData"
android:valueFrom="M 398.12 265.85 C 445.48 227 470.65 176.31 511.63 120.83 C 519.36 110.37 477.05 85.13 460.32 83.46 C 443.59 81.79 429.55 143.25 427.97 179.4 C 426.53 212.41 391.76 271.08 398.13 265.85 Z"
android:valueTo="M 360 120 C 342 120 330 132 330 150 C 330 168 342 180 360 180 C 378 180 390 168 390 150 C 390 132 378 120 360 120 Z"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="240"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_3">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="300"
android:interpolator="@android:anim/anticipate_interpolator"
android:propertyName="pathData"
android:valueFrom="M 487.06 423.67 C 450.04 417.37 435.82 366.44 399.15 355.87 L 399.15 355.87 C 393.715 354.305 388.563 354.31 383.891 355.198 C 379.219 356.086 375.029 357.856 371.517 359.822 C 368.006 361.789 365.174 363.951 363.22 365.621 C 361.266 367.291 360.19 368.47 360.19 368.47 C 360.19 368.47 360.291 371.528 360.96 375.629 C 361.63 379.73 362.867 384.874 365.14 389.048 C 367.412 393.221 370.72 396.423 375.53 396.64 C 415.49 398.45 422.5 458.03 482.46 457.15 C 564.32 455.95 596.97 356.99 587.59 370.29 C 555.88 415.28 532.14 425.89 513.37 426.79 C 503.33 427.27 494.71 424.97 487.06 423.67"
android:valueTo="M 540 120 C 515 120 490 120 465 120 L 444 99 C 438 93 432 90 423 90 C 381 90 339 90 297 90 C 288 90 282 93 276 99 C 269 106 262 113 255 120 C 230 120 205 120 180 120 C 162 120 150 135 150 150 C 150 165 162 180 180 180 C 300 180 420 180 540 180 C 555 180 570 168 570 150 C 570 132 555 120 540 120 C 540 120 540 120 540 120"
android:valueType="pathType" />
<objectAnimator
android:duration="10"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="290"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="lid">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:anim/anticipate_interpolator"
android:propertyName="pathData"
android:valueFrom="M 357.44 365.341 C 355.486 363.671 352.654 361.509 349.142 359.543 C 345.631 357.576 341.441 355.806 336.769 354.918 C 332.098 354.03 326.945 354.025 321.51 355.59 L 321.51 355.59 C 284.84 366.16 270.62 417.09 233.6 423.39 C 225.95 424.69 217.33 426.99 207.29 426.51 C 188.52 425.61 164.78 415 133.07 370.01 C 123.69 356.71 156.34 455.67 238.2 456.87 C 258.187 457.163 272.29 450.739 283.691 441.976 C 295.093 433.212 303.793 422.11 312.972 413.048 C 322.151 403.986 331.81 396.963 345.13 396.36 C 359.56 395.71 360.47 368.19 360.47 368.19 C 360.47 368.19 359.394 367.011 357.44 365.341"
android:valueTo="M 540 120 C 515 120 490 120 465 120 C 458 113 451 106 444 99 C 438 93 432 90 423 90 L 297 90 C 288 90 282 93 276 99 C 269 106 262 113 255 120 C 230 120 205 120 180 120 C 162 120 150 135 150 150 C 150 165 162 180 180 180 C 300 180 420 180 540 180 C 555 180 570 168 570 150 C 570 132 555 120 540 120 C 540 120 540 120 540 120"
android:valueType="pathType" />
</aapt:attr>
</target>
<target android:name="trash_group">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:anim/anticipate_interpolator"
android:propertyName="translateY"
android:startOffset="600"
android:valueFrom="0"
android:valueTo="200"
android:valueType="floatType" />
</aapt:attr>
</target>
<target android:name="lid_group">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="30"
android:valueType="floatType" />
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="translateX"
android:valueFrom="0"
android:valueTo="300"
android:valueType="floatType" />
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="translateY"
android:valueFrom="0"
android:valueTo="-200"
android:valueType="floatType" />
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="translateX"
android:startOffset="900"
android:valueFrom="300"
android:valueTo="0"
android:valueType="floatType" />
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="rotation"
android:startOffset="900"
android:valueFrom="30"
android:valueTo="0"
android:valueType="floatType" />
<objectAnimator
android:duration="300"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="translateY"
android:startOffset="900"
android:valueFrom="-200"
android:valueTo="0"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="outlined"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 23 13.5 C 23 14.163 22.736 14.799 22.268 15.268 C 21.799 15.736 21.163 16 20.5 16 C 20 16 19.5 16 19 16 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 15.1 22 L 13.2 22 C 13.2 21.5 13.2 21 13.2 20.5 C 13.2 19 12 17.8 10.5 17.8 C 9 17.8 7.8 19 7.8 20.5 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 3.5 16.2 C 5 16.2 6.2 15 6.2 13.5 C 6.2 12 5 10.8 3.5 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 8 5 C 8 4.5 8 4 8 3.5 C 8 2.837 8.264 2.201 8.732 1.732 C 9.201 1.264 9.837 1 10.5 1 C 11.163 1 11.799 1.264 12.268 1.732 C 12.736 2.201 13 2.837 13 3.5 C 13 4 13 4.5 13 5 L 17 5 C 17.55 5 18.05 5.223 18.413 5.584 C 18.775 5.945 19 6.445 19 7 L 19 11 C 19.5 11 20 11 20.5 11 C 20.5 11 20.5 11 20.5 11 C 21.163 11 21.799 11.264 22.268 11.732 C 22.736 12.201 23 12.837 23 13.5 M 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12" />
</vector>
</aapt:attr>
<target android:name="outlined">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 23 13.5 C 23 14.163 22.736 14.799 22.268 15.268 C 21.799 15.736 21.163 16 20.5 16 C 20 16 19.5 16 19 16 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 15.1 22 L 13.2 22 C 13.2 21.5 13.2 21 13.2 20.5 C 13.2 19 12 17.8 10.5 17.8 C 9 17.8 7.8 19 7.8 20.5 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 3.5 16.2 C 5 16.2 6.2 15 6.2 13.5 C 6.2 12 5 10.8 3.5 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 8 5 C 8 4.5 8 4 8 3.5 C 8 2.837 8.264 2.201 8.732 1.732 C 9.201 1.264 9.837 1 10.5 1 C 11.163 1 11.799 1.264 12.268 1.732 C 12.736 2.201 13 2.837 13 3.5 C 13 4 13 4.5 13 5 L 17 5 C 17.55 5 18.05 5.223 18.413 5.584 C 18.775 5.945 19 6.445 19 7 L 19 11 C 19.5 11 20 11 20.5 11 C 20.5 11 20.5 11 20.5 11 C 21.163 11 21.799 11.264 22.268 11.732 C 22.736 12.201 23 12.837 23 13.5 M 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12"
android:valueTo="M 22 13.5 C 22 14.087 21.856 14.64 21.6 15.126 C 21.344 15.612 20.978 16.03 20.533 16.347 C 20.089 16.664 19.567 16.88 19 16.96 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 13.2 22 L 13.2 21.7 C 13.2 20.984 12.915 20.297 12.409 19.791 C 11.903 19.285 11.216 19 10.5 19 C 9 19 7.8 20.21 7.8 21.7 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 2.3 16.2 C 3.79 16.2 5 15 5 13.5 C 5 12 3.79 10.8 2.3 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 7.04 5 C 7.12 4.433 7.336 3.911 7.653 3.467 C 7.97 3.022 8.388 2.656 8.874 2.4 C 9.36 2.144 9.913 2 10.5 2 C 11.087 2 11.64 2.144 12.126 2.4 C 12.612 2.656 13.03 3.022 13.347 3.467 C 13.664 3.911 13.88 4.433 13.96 5 L 17 5 C 17.53 5 18.039 5.211 18.414 5.586 C 18.789 5.961 19 6.47 19 7 L 19 10.04 C 19.425 10.1 19.825 10.236 20.186 10.434 C 20.547 10.633 20.869 10.893 21.137 11.2 C 21.406 11.508 21.622 11.863 21.77 12.251 C 21.919 12.639 22 13.06 22 13.5 M 17 12 L 18.5 12 C 18.898 12 19.279 12.158 19.561 12.439 C 19.842 12.721 20 13.102 20 13.5 C 20 13.898 19.842 14.279 19.561 14.561 C 19.279 14.842 18.898 15 18.5 15 L 17 15 L 17 15 L 17 20 L 14.88 20 C 14.2 18.25 12.5 17 10.5 17 C 8.5 17 6.8 18.25 6.12 20 L 4 20 L 4 17.88 C 5.75 17.2 7 15.5 7 13.5 C 7 11.5 5.76 9.8 4 9.12 L 4 7 L 9 7 L 9 5.5 C 9 5.102 9.158 4.721 9.439 4.439 C 9.721 4.158 10.102 4 10.5 4 C 10.898 4 11.279 4.158 11.561 4.439 C 11.842 4.721 12 5.102 12 5.5 L 12 7 L 17 7 L 17 12"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="outlined"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 22 13.5 C 22 15.26 20.7 16.72 19 16.96 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 13.2 22 L 13.2 21.7 C 13.2 20.984 12.915 20.297 12.409 19.791 C 11.903 19.285 11.216 19 10.5 19 C 9 19 7.8 20.21 7.8 21.7 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 2.3 16.2 C 3.79 16.2 5 15 5 13.5 C 5 12 3.79 10.8 2.3 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 7.04 5 C 7.28 3.3 8.74 2 10.5 2 C 12.26 2 13.72 3.3 13.96 5 L 17 5 C 17.53 5 18.039 5.211 18.414 5.586 C 18.789 5.961 19 6.47 19 7 L 19 10.04 C 20.7 10.28 22 11.74 22 13.5 M 17 15 L 18.5 15 C 18.898 15 19.279 14.842 19.561 14.561 C 19.842 14.279 20 13.898 20 13.5 C 20 13.102 19.842 12.721 19.561 12.439 C 19.279 12.158 18.898 12 18.5 12 L 17 12 L 17 7 L 12 7 L 12 5.5 C 12 5.102 11.842 4.721 11.561 4.439 C 11.279 4.158 10.898 4 10.5 4 C 10.102 4 9.721 4.158 9.439 4.439 C 9.158 4.721 9 5.102 9 5.5 L 9 7 L 4 7 L 4 9.12 C 5.76 9.8 7 11.5 7 13.5 C 7 15.5 5.75 17.2 4 17.88 L 4 20 L 6.12 20 C 6.8 18.25 8.5 17 10.5 17 C 12.5 17 14.2 18.25 14.88 20 L 17 20 L 17 15 Z" />
</vector>
</aapt:attr>
<target android:name="outlined">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 22 13.5 C 22 14.087 21.856 14.64 21.6 15.126 C 21.344 15.612 20.978 16.03 20.533 16.347 C 20.089 16.664 19.567 16.88 19 16.96 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 13.2 22 L 13.2 21.7 C 13.2 20.984 12.915 20.297 12.409 19.791 C 11.903 19.285 11.216 19 10.5 19 C 9 19 7.8 20.21 7.8 21.7 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 2.3 16.2 C 3.79 16.2 5 15 5 13.5 C 5 12 3.79 10.8 2.3 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 7.04 5 C 7.12 4.433 7.336 3.911 7.653 3.467 C 7.97 3.022 8.388 2.656 8.874 2.4 C 9.36 2.144 9.913 2 10.5 2 C 11.087 2 11.64 2.144 12.126 2.4 C 12.612 2.656 13.03 3.022 13.347 3.467 C 13.664 3.911 13.88 4.433 13.96 5 L 17 5 C 17.53 5 18.039 5.211 18.414 5.586 C 18.789 5.961 19 6.47 19 7 L 19 10.04 C 19.425 10.1 19.825 10.236 20.186 10.434 C 20.547 10.633 20.869 10.893 21.137 11.2 C 21.406 11.508 21.622 11.863 21.77 12.251 C 21.919 12.639 22 13.06 22 13.5 M 17 12 L 18.5 12 C 18.898 12 19.279 12.158 19.561 12.439 C 19.842 12.721 20 13.102 20 13.5 C 20 13.898 19.842 14.279 19.561 14.561 C 19.279 14.842 18.898 15 18.5 15 L 17 15 L 17 15 L 17 20 L 14.88 20 C 14.2 18.25 12.5 17 10.5 17 C 8.5 17 6.8 18.25 6.12 20 L 4 20 L 4 17.88 C 5.75 17.2 7 15.5 7 13.5 C 7 11.5 5.76 9.8 4 9.12 L 4 7 L 9 7 L 9 5.5 C 9 5.102 9.158 4.721 9.439 4.439 C 9.721 4.158 10.102 4 10.5 4 C 10.898 4 11.279 4.158 11.561 4.439 C 11.842 4.721 12 5.102 12 5.5 L 12 7 L 17 7 L 17 12"
android:valueTo="M 23 13.5 C 23 14.163 22.736 14.799 22.268 15.268 C 21.799 15.736 21.163 16 20.5 16 C 20 16 19.5 16 19 16 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 15.1 22 L 13.2 22 C 13.2 21.5 13.2 21 13.2 20.5 C 13.2 19 12 17.8 10.5 17.8 C 9 17.8 7.8 19 7.8 20.5 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 3.5 16.2 C 5 16.2 6.2 15 6.2 13.5 C 6.2 12 5 10.8 3.5 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 8 5 C 8 4.5 8 4 8 3.5 C 8 2.837 8.264 2.201 8.732 1.732 C 9.201 1.264 9.837 1 10.5 1 C 11.163 1 11.799 1.264 12.268 1.732 C 12.736 2.201 13 2.837 13 3.5 C 13 4 13 4.5 13 5 L 17 5 C 17.55 5 18.05 5.223 18.413 5.584 C 18.775 5.945 19 6.445 19 7 L 19 11 C 19.5 11 20 11 20.5 11 C 20.5 11 20.5 11 20.5 11 C 21.163 11 21.799 11.264 22.268 11.732 C 22.736 12.201 23 12.837 23 13.5 M 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 12 15.5 C 11.072 15.5 10.181 15.131 9.525 14.475 C 8.869 13.819 8.5 12.928 8.5 12 C 8.5 11.072 8.869 10.181 9.525 9.525 C 10.181 8.869 11.072 8.5 12 8.5 C 12.614 8.5 13.218 8.662 13.75 8.969 C 14.282 9.276 14.724 9.718 15.031 10.25 C 15.338 10.782 15.5 11.386 15.5 12 C 15.5 12.614 15.338 13.218 15.031 13.75 C 14.724 14.282 14.282 14.724 13.75 15.031 C 13.218 15.338 12.614 15.5 12 15.5 M 19.43 12.97 C 19.47 12.65 19.5 12.33 19.5 12 C 19.5 11.67 19.47 11.34 19.43 11 L 21.54 9.37 C 21.73 9.22 21.78 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.53 11.34 4.5 11.67 4.5 12 C 4.5 12.33 4.53 12.65 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.67 16.04 18.34 16.56 17.94 L 19.05 18.95 C 19.27 19.03 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.78 15.05 21.73 14.78 21.54 14.63 L 19.43 12.97 Z" />
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.53 11.34 4.5 11.67 4.5 12 C 4.5 12.33 4.53 12.65 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.67 16.04 18.34 16.56 17.94 L 19.05 18.95 C 19.27 19.03 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.78 15.05 21.73 14.78 21.54 14.63 L 19.43 12.97 L 19.43 12.97 C 19.47 12.65 19.5 12.33 19.5 12 C 19.5 11.67 19.47 11.34 19.43 11 L 21.54 9.37 C 21.73 9.22 21.78 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8.5 C 12.614 8.5 13.218 8.662 13.75 8.969 C 14.282 9.276 14.724 9.718 15.031 10.25 C 15.338 10.782 15.5 11.386 15.5 12 C 15.5 12.614 15.338 13.218 15.031 13.75 C 14.724 14.282 14.282 14.724 13.75 15.031 C 13.218 15.338 12.614 15.5 12 15.5 C 11.072 15.5 10.181 15.131 9.525 14.475 C 8.869 13.819 8.5 12.928 8.5 12 C 8.5 11.072 8.869 10.181 9.525 9.525 C 10.181 8.869 11.072 8.5 12 8.5 M 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 M 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12"
android:valueTo="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.547 11.333 4.523 11.667 4.5 12 C 4.523 12.323 4.547 12.647 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.68 16.04 18.34 16.56 17.95 L 19.05 18.95 C 19.27 19.04 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.79 15.05 21.73 14.78 21.54 14.63 L 19.43 13 L 19.465 12.499 C 19.477 12.333 19.488 12.166 19.5 12 C 19.477 11.667 19.453 11.333 19.43 11 L 21.54 9.37 C 21.73 9.22 21.79 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8 C 12.53 8 13.05 8.105 13.531 8.305 C 14.011 8.504 14.454 8.797 14.828 9.172 C 15.578 9.921 16 10.94 16 12 C 16 12.53 15.895 13.05 15.695 13.531 C 15.496 14.011 15.203 14.454 14.828 14.828 C 14.079 15.578 13.06 16 12 16 C 10.94 16 9.921 15.578 9.172 14.828 C 8.422 14.079 8 13.06 8 12 C 8 10.94 8.422 9.921 9.172 9.172 C 9.921 8.422 10.94 8 12 8 M 12 10 C 11.912 10 11.824 10.006 11.737 10.017 C 11.651 10.029 11.565 10.046 11.481 10.069 C 11.397 10.091 11.315 10.119 11.235 10.152 C 11.155 10.186 11.077 10.224 11.001 10.267 C 10.926 10.311 10.854 10.359 10.784 10.412 C 10.715 10.466 10.649 10.524 10.586 10.586 C 10.524 10.649 10.466 10.715 10.412 10.784 C 10.359 10.854 10.311 10.926 10.267 11.001 C 10.224 11.077 10.186 11.155 10.152 11.235 C 10.119 11.315 10.091 11.397 10.069 11.481 C 10.046 11.565 10.029 11.651 10.017 11.737 C 10.006 11.824 10 11.912 10 12 C 10 12.088 10.006 12.176 10.017 12.263 C 10.029 12.349 10.046 12.435 10.069 12.519 C 10.091 12.603 10.119 12.685 10.152 12.765 C 10.186 12.845 10.224 12.923 10.267 12.999 C 10.311 13.074 10.359 13.146 10.412 13.216 C 10.466 13.285 10.524 13.351 10.586 13.414 C 10.649 13.476 10.715 13.534 10.784 13.588 C 10.854 13.641 10.926 13.689 11.001 13.733 C 11.077 13.776 11.155 13.814 11.235 13.848 C 11.315 13.881 11.397 13.909 11.481 13.931 C 11.565 13.954 11.651 13.971 11.737 13.983 C 11.824 13.994 11.912 14 12 14 C 12.53 14 13.039 13.789 13.414 13.414 C 13.468 13.36 13.518 13.304 13.565 13.245 C 13.611 13.187 13.655 13.126 13.694 13.062 C 13.734 12.999 13.77 12.934 13.802 12.867 C 13.834 12.8 13.863 12.731 13.887 12.661 C 13.912 12.591 13.933 12.519 13.949 12.447 C 13.966 12.374 13.979 12.3 13.987 12.226 C 13.996 12.151 14 12.076 14 12 C 14 11.912 13.994 11.824 13.983 11.737 C 13.971 11.651 13.954 11.565 13.931 11.481 C 13.909 11.397 13.881 11.315 13.848 11.235 C 13.814 11.155 13.776 11.077 13.733 11.001 C 13.689 10.926 13.641 10.854 13.588 10.784 C 13.534 10.715 13.476 10.649 13.414 10.586 C 13.039 10.211 12.53 10 12 10 M 11.25 4 L 11.25 4 L 12.75 4 L 13.12 6.62 C 14.32 6.86 15.38 7.5 16.15 8.39 L 18.56 7.35 L 19.31 8.65 L 17.2 10.2 C 17.6 11.37 17.6 12.64 17.2 13.81 L 19.32 15.36 L 18.57 16.66 L 16.14 15.62 C 15.37 16.5 14.32 17.14 13.13 17.39 L 12.76 20 L 11.24 20 L 10.87 17.38 C 9.68 17.14 8.63 16.5 7.86 15.62 L 5.43 16.66 L 4.68 15.36 L 6.8 13.8 C 6.4 12.64 6.4 11.37 6.8 10.2 L 4.69 8.65 L 5.44 7.35 L 7.85 8.39 C 8.62 7.5 9.68 6.86 10.88 6.61 L 11.25 4"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.547 11.333 4.523 11.667 4.5 12 C 4.523 12.323 4.547 12.647 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.68 16.04 18.34 16.56 17.95 L 19.05 18.95 C 19.27 19.04 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.79 15.05 21.73 14.78 21.54 14.63 L 19.43 13 L 19.465 12.499 C 19.477 12.333 19.488 12.166 19.5 12 C 19.477 11.667 19.453 11.333 19.43 11 L 21.54 9.37 C 21.73 9.22 21.79 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8 C 12.53 8 13.05 8.105 13.531 8.305 C 14.011 8.504 14.454 8.797 14.828 9.172 C 15.578 9.921 16 10.94 16 12 C 16 12.53 15.895 13.05 15.695 13.531 C 15.496 14.011 15.203 14.454 14.828 14.828 C 14.079 15.578 13.06 16 12 16 C 10.94 16 9.921 15.578 9.172 14.828 C 8.422 14.079 8 13.06 8 12 C 8 10.94 8.422 9.921 9.172 9.172 C 9.921 8.422 10.94 8 12 8 M 12 10 C 11.912 10 11.824 10.006 11.737 10.017 C 11.651 10.029 11.565 10.046 11.481 10.069 C 11.397 10.091 11.315 10.119 11.235 10.152 C 11.155 10.186 11.077 10.224 11.001 10.267 C 10.926 10.311 10.854 10.359 10.784 10.412 C 10.715 10.466 10.649 10.524 10.586 10.586 C 10.524 10.649 10.466 10.715 10.412 10.784 C 10.359 10.854 10.311 10.926 10.267 11.001 C 10.224 11.077 10.186 11.155 10.152 11.235 C 10.119 11.315 10.091 11.397 10.069 11.481 C 10.046 11.565 10.029 11.651 10.017 11.737 C 10.006 11.824 10 11.912 10 12 C 10 12.088 10.006 12.176 10.017 12.263 C 10.029 12.349 10.046 12.435 10.069 12.519 C 10.091 12.603 10.119 12.685 10.152 12.765 C 10.186 12.845 10.224 12.923 10.267 12.999 C 10.311 13.074 10.359 13.146 10.412 13.216 C 10.466 13.285 10.524 13.351 10.586 13.414 C 10.649 13.476 10.715 13.534 10.784 13.588 C 10.854 13.641 10.926 13.689 11.001 13.733 C 11.077 13.776 11.155 13.814 11.235 13.848 C 11.315 13.881 11.397 13.909 11.481 13.931 C 11.565 13.954 11.651 13.971 11.737 13.983 C 11.824 13.994 11.912 14 12 14 C 12.53 14 13.039 13.789 13.414 13.414 C 13.468 13.36 13.518 13.304 13.565 13.245 C 13.611 13.187 13.655 13.126 13.694 13.062 C 13.734 12.999 13.77 12.934 13.802 12.867 C 13.834 12.8 13.863 12.731 13.887 12.661 C 13.912 12.591 13.933 12.519 13.949 12.447 C 13.966 12.374 13.979 12.3 13.987 12.226 C 13.996 12.151 14 12.076 14 12 C 14 11.912 13.994 11.824 13.983 11.737 C 13.971 11.651 13.954 11.565 13.931 11.481 C 13.909 11.397 13.881 11.315 13.848 11.235 C 13.814 11.155 13.776 11.077 13.733 11.001 C 13.689 10.926 13.641 10.854 13.588 10.784 C 13.534 10.715 13.476 10.649 13.414 10.586 C 13.039 10.211 12.53 10 12 10 M 11.25 4 L 11.25 4 L 12.75 4 L 13.12 6.62 C 14.32 6.86 15.38 7.5 16.15 8.39 L 18.56 7.35 L 19.31 8.65 L 17.2 10.2 C 17.6 11.37 17.6 12.64 17.2 13.81 L 19.32 15.36 L 18.57 16.66 L 16.14 15.62 C 15.37 16.5 14.32 17.14 13.13 17.39 L 12.76 20 L 11.24 20 L 10.87 17.38 C 9.68 17.14 8.63 16.5 7.86 15.62 L 5.43 16.66 L 4.68 15.36 L 6.8 13.8 C 6.4 12.64 6.4 11.37 6.8 10.2 L 4.69 8.65 L 5.44 7.35 L 7.85 8.39 C 8.62 7.5 9.68 6.86 10.88 6.61 L 11.25 4" />
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.547 11.333 4.523 11.667 4.5 12 C 4.523 12.323 4.547 12.647 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.68 16.04 18.34 16.56 17.95 L 19.05 18.95 C 19.27 19.04 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.79 15.05 21.73 14.78 21.54 14.63 L 19.43 13 L 19.465 12.499 C 19.477 12.333 19.488 12.166 19.5 12 C 19.477 11.667 19.453 11.333 19.43 11 L 21.54 9.37 C 21.73 9.22 21.79 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8 C 12.53 8 13.05 8.105 13.531 8.305 C 14.011 8.504 14.454 8.797 14.828 9.172 C 15.578 9.921 16 10.94 16 12 C 16 12.53 15.895 13.05 15.695 13.531 C 15.496 14.011 15.203 14.454 14.828 14.828 C 14.079 15.578 13.06 16 12 16 C 10.94 16 9.921 15.578 9.172 14.828 C 8.422 14.079 8 13.06 8 12 C 8 10.94 8.422 9.921 9.172 9.172 C 9.921 8.422 10.94 8 12 8 M 12 10 C 11.912 10 11.824 10.006 11.737 10.017 C 11.651 10.029 11.565 10.046 11.481 10.069 C 11.397 10.091 11.315 10.119 11.235 10.152 C 11.155 10.186 11.077 10.224 11.001 10.267 C 10.926 10.311 10.854 10.359 10.784 10.412 C 10.715 10.466 10.649 10.524 10.586 10.586 C 10.524 10.649 10.466 10.715 10.412 10.784 C 10.359 10.854 10.311 10.926 10.267 11.001 C 10.224 11.077 10.186 11.155 10.152 11.235 C 10.119 11.315 10.091 11.397 10.069 11.481 C 10.046 11.565 10.029 11.651 10.017 11.737 C 10.006 11.824 10 11.912 10 12 C 10 12.088 10.006 12.176 10.017 12.263 C 10.029 12.349 10.046 12.435 10.069 12.519 C 10.091 12.603 10.119 12.685 10.152 12.765 C 10.186 12.845 10.224 12.923 10.267 12.999 C 10.311 13.074 10.359 13.146 10.412 13.216 C 10.466 13.285 10.524 13.351 10.586 13.414 C 10.649 13.476 10.715 13.534 10.784 13.588 C 10.854 13.641 10.926 13.689 11.001 13.733 C 11.077 13.776 11.155 13.814 11.235 13.848 C 11.315 13.881 11.397 13.909 11.481 13.931 C 11.565 13.954 11.651 13.971 11.737 13.983 C 11.824 13.994 11.912 14 12 14 C 12.53 14 13.039 13.789 13.414 13.414 C 13.468 13.36 13.518 13.304 13.565 13.245 C 13.611 13.187 13.655 13.126 13.694 13.062 C 13.734 12.999 13.77 12.934 13.802 12.867 C 13.834 12.8 13.863 12.731 13.887 12.661 C 13.912 12.591 13.933 12.519 13.949 12.447 C 13.966 12.374 13.979 12.3 13.987 12.226 C 13.996 12.151 14 12.076 14 12 C 14 11.912 13.994 11.824 13.983 11.737 C 13.971 11.651 13.954 11.565 13.931 11.481 C 13.909 11.397 13.881 11.315 13.848 11.235 C 13.814 11.155 13.776 11.077 13.733 11.001 C 13.689 10.926 13.641 10.854 13.588 10.784 C 13.534 10.715 13.476 10.649 13.414 10.586 C 13.039 10.211 12.53 10 12 10 M 11.25 4 L 11.25 4 L 12.75 4 L 13.12 6.62 C 14.32 6.86 15.38 7.5 16.15 8.39 L 18.56 7.35 L 19.31 8.65 L 17.2 10.2 C 17.6 11.37 17.6 12.64 17.2 13.81 L 19.32 15.36 L 18.57 16.66 L 16.14 15.62 C 15.37 16.5 14.32 17.14 13.13 17.39 L 12.76 20 L 11.24 20 L 10.87 17.38 C 9.68 17.14 8.63 16.5 7.86 15.62 L 5.43 16.66 L 4.68 15.36 L 6.8 13.8 C 6.4 12.64 6.4 11.37 6.8 10.2 L 4.69 8.65 L 5.44 7.35 L 7.85 8.39 C 8.62 7.5 9.68 6.86 10.88 6.61 L 11.25 4"
android:valueTo="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.53 11.34 4.5 11.67 4.5 12 C 4.5 12.33 4.53 12.65 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.67 16.04 18.34 16.56 17.94 L 19.05 18.95 C 19.27 19.03 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.78 15.05 21.73 14.78 21.54 14.63 L 19.43 12.97 L 19.43 12.97 C 19.47 12.65 19.5 12.33 19.5 12 C 19.5 11.67 19.47 11.34 19.43 11 L 21.54 9.37 C 21.73 9.22 21.78 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8.5 C 12.614 8.5 13.218 8.662 13.75 8.969 C 14.282 9.276 14.724 9.718 15.031 10.25 C 15.338 10.782 15.5 11.386 15.5 12 C 15.5 12.614 15.338 13.218 15.031 13.75 C 14.724 14.282 14.282 14.724 13.75 15.031 C 13.218 15.338 12.614 15.5 12 15.5 C 11.072 15.5 10.181 15.131 9.525 14.475 C 8.869 13.819 8.5 12.928 8.5 12 C 8.5 11.072 8.869 10.181 9.525 9.525 C 10.181 8.869 11.072 8.5 12 8.5 M 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 M 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 12 1 L 3 5 L 3 11 C 3 16.55 6.84 21.74 12 23 C 17.16 21.74 21 16.55 21 11 L 21 5 L 12 1 Z" />
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 12 1 L 12 1 L 21 5 L 21 11 M 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18"
android:valueTo="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 7.5 3 L 12 1 L 21 5 L 21 11 M 12 21 L 12 21 C 8.25 20 5 15.54 5 11.22 L 5 6.3 L 12 3.18 L 19 6.3 L 19 11.22 C 19 15.54 15.75 20 12 21"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 7.5 3 L 12 1 L 21 5 L 21 11 M 12 21 L 12 21 C 8.25 20 5 15.54 5 11.22 L 5 6.3 L 12 3.18 L 19 6.3 L 19 11.22 C 19 15.54 15.75 20 12 21" />
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 7.5 3 L 12 1 L 21 5 L 21 11 M 12 21 L 12 21 C 8.25 20 5 15.54 5 11.22 L 5 6.3 L 12 3.18 L 19 6.3 L 19 11.22 C 19 15.54 15.75 20 12 21"
android:valueTo="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 12 1 L 12 1 L 21 5 L 21 11 M 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/black" />
<corners android:bottomLeftRadius="2dp" android:bottomRightRadius="2dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/black" />
<corners android:topLeftRadius="2dp" android:topRightRadius="2dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="#43A047" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorOnSurface"
android:pathData="M20,11H6.83l2.88,-2.88c0.39,-0.39 0.39,-1.02 0,-1.41 -0.39,-0.39 -1.02,-0.39 -1.41,0L3.71,11.3c-0.39,0.39 -0.39,1.02 0,1.41L8.3,17.3c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L6.83,13H20c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorOnSurface"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z" />
</vector>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/checked"
android:drawable="@drawable/ic_bug_filled_md2"
android:state_checked="true" />
<item
android:id="@+id/unchecked"
android:drawable="@drawable/ic_bug_outlined_md2" />
<transition
android:drawable="@drawable/avd_bug_from_filled"
android:fromId="@+id/checked"
android:toId="@+id/unchecked" />
<transition
android:drawable="@drawable/avd_bug_to_filled"
android:fromId="@+id/unchecked"
android:toId="@id/checked" />
</animated-selector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorOnSurface"
android:pathData="M20,8H17.19C16.74,7.2 16.12,6.5 15.37,6L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.05,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6C7.87,6.5 7.26,7.21 6.81,8H4V10H6.09C6.03,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.03,15.67 6.09,16H4V18H6.81C8.47,20.87 12.14,21.84 15,20.18C15.91,19.66 16.67,18.9 17.19,18H20V16H17.91C17.97,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.97,10.33 17.91,10H20V8M16,15A4,4 0 0,1 12,19A4,4 0 0,1 8,15V11A4,4 0 0,1 12,7A4,4 0 0,1 16,11V15M14,10V12H10V10H14M10,14H14V16H10V14Z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorOnSurface"
android:pathData="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z" />
</vector>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="NewApi">
<item
android:id="@+id/checked"
android:drawable="@drawable/ic_check_circle_checked_md2"
android:state_selected="true" />
<item
android:id="@+id/unchecked"
android:drawable="@drawable/ic_check_circle_unchecked_md2" />
<transition
android:drawable="@drawable/avd_circle_check_from_filled"
android:fromId="@+id/checked"
android:toId="@+id/unchecked" />
<transition
android:drawable="@drawable/avd_circle_check_to_filled"
android:fromId="@+id/unchecked"
android:toId="@id/checked" />
</animated-selector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorOnSurface"
android:pathData="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M7,13H17V11H7" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorOnSurface"
android:pathData="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"
tools:fillColor="#000" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorOnSurface"
android:pathData="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
tools:fillColor="#000" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorOnSurface"
android:pathData="M3.55,18.54L4.96,19.95L6.76,18.16L5.34,16.74M11,22.45C11.32,22.45 13,22.45 13,22.45V19.5H11M12,5.5A6,6 0 0,0 6,11.5A6,6 0 0,0 12,17.5A6,6 0 0,0 18,11.5C18,8.18 15.31,5.5 12,5.5M20,12.5H23V10.5H20M17.24,18.16L19.04,19.95L20.45,18.54L18.66,16.74M20.45,4.46L19.04,3.05L17.24,4.84L18.66,6.26M13,0.55H11V3.5H13M4,10.5H1V12.5H4M6.76,4.84L4.96,3.05L3.55,4.46L5.34,6.26L6.76,4.84Z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorOnSurface"
android:pathData="M7.5,2C5.71,3.15 4.5,5.18 4.5,7.5C4.5,9.82 5.71,11.85 7.53,13C4.46,13 2,10.54 2,7.5A5.5,5.5 0 0,1 7.5,2M19.07,3.5L20.5,4.93L4.93,20.5L3.5,19.07L19.07,3.5M12.89,5.93L11.41,5L9.97,6L10.39,4.3L9,3.24L10.75,3.12L11.33,1.47L12,3.1L13.73,3.13L12.38,4.26L12.89,5.93M9.59,9.54L8.43,8.81L7.31,9.59L7.65,8.27L6.56,7.44L7.92,7.35L8.37,6.06L8.88,7.33L10.24,7.36L9.19,8.23L9.59,9.54M19,13.5A5.5,5.5 0 0,1 13.5,19C12.28,19 11.15,18.6 10.24,17.93L17.93,10.24C18.6,11.15 19,12.28 19,13.5M14.6,20.08L17.37,18.93L17.13,22.28L14.6,20.08M18.93,17.38L20.08,14.61L22.28,17.15L18.93,17.38M20.08,12.42L18.94,9.64L22.28,9.88L20.08,12.42M9.63,18.93L12.4,20.08L9.87,22.27L9.63,18.93Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<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="?colorOnSurface"
android:pathData="M18,4h-2.5l-0.7,-0.7C14.6,3.2 14.4,3 14.1,3H9.9C9.6,3 9.4,3.2 9.2,3.3L8.5,4H6C5.4,4 5,4.5 5,5s0.4,1 1,1h12c0.5,0 1,-0.4 1,-1S18.5,4 18,4z" />
<path
android:fillColor="?colorOnSurface"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V9c0,-1.1 -0.9,-2 -2,-2H8C6.9,7 6,7.9 6,9V19z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorOnSurface"
android:pathData="M17,19V5H7V19H17M17,1A2,2 0 0,1 19,3V21A2,2 0 0,1 17,23H7C5.89,23 5,22.1 5,21V3C5,1.89 5.89,1 7,1H17M9,7H15V9H9V7M9,11H13V13H9V11Z" />
</vector>

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