Commit c1dad11c authored by topjohnwu's avatar topjohnwu

Merge branch 'md2'

parents 12b219e7 12a9792c
......@@ -17,3 +17,4 @@ tools/** binary
*.apk binary
*.png binary
*.jpg binary
*.ttf binary
......@@ -19,6 +19,12 @@ android {
multiDexEnabled true
versionName props['appVersion']
versionCode props['appVersionCode'] as Integer
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.incremental":"true"]
}
}
}
buildTypes {
......@@ -62,7 +68,7 @@ dependencies {
implementation 'com.ncapdevi:frag-nav:3.2.0'
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6'
implementation 'io.reactivex.rxjava2:rxjava:2.2.13'
implementation 'io.reactivex.rxjava2:rxjava:2.2.16'
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
......@@ -89,14 +95,16 @@ dependencies {
implementation "org.koin:koin-android:${vKoin}"
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
def vRetrofit = '2.6.2'
def vRetrofit = '2.7.1'
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
def vOkHttp = '3.12.6'
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
def vOkHttp = '3.12.7'
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}") {
force = true
}
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
def vMoshi = '1.9.2'
......@@ -111,7 +119,7 @@ dependencies {
replacedBy('com.github.topjohnwu:room-runtime')
}
}
def vRoom = '2.2.2'
def vRoom = '2.2.3'
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
implementation "androidx.room:room-rxjava2:${vRoom}"
kapt "androidx.room:room-compiler:${vRoom}"
......@@ -120,16 +128,16 @@ dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:${vNav}"
implementation "androidx.navigation:navigation-ui-ktx:${vNav}"
implementation 'androidx.biometric:biometric:1.0.0'
implementation 'androidx.biometric:biometric:1.0.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.fragment:fragment-ktx:1.2.0-rc03'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.fragment:fragment-ktx:1.2.0-rc05'
implementation 'androidx.work:work-runtime:2.2.0'
implementation 'androidx.transition:transition:1.3.0-rc02'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'com.google.android.material:material:1.2.0-alpha02'
implementation 'androidx.core:core-ktx:1.2.0-rc01'
implementation 'com.google.android.material:material:1.2.0-alpha03'
}
......@@ -17,32 +17,26 @@
#}
# Snet
-keepclassmembers class com.topjohnwu.magisk.utils.SafetyNetHelper { *; }
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.SafetyNetHelper$Callback
-keepclassmembers class * implements com.topjohnwu.magisk.utils.SafetyNetHelper$Callback {
-keepclassmembers class com.topjohnwu.magisk.core.utils.SafetyNetHelper { *; }
-keep,allowobfuscation interface com.topjohnwu.magisk.core.utils.SafetyNetHelper$Callback
-keepclassmembers class * implements com.topjohnwu.magisk.core.utils.SafetyNetHelper$Callback {
void onResponse(int);
}
# Keep all fragment constructors
-keepclassmembers class * extends androidx.fragment.app.Fragment {
public <init>(...);
}
# Fragments
-keep,allowobfuscation class * extends androidx.fragment.app.Fragment
# DelegateWorker
-keep,allowobfuscation class * extends com.topjohnwu.magisk.base.DelegateWorker
# BaseWorkerWrapper
-keep,allowobfuscation class * extends com.topjohnwu.magisk.core.base.BaseWorkerWrapper
# BootSigner
-keep class a.a { *; }
# Workaround R8 bug
-keep,allowobfuscation class com.topjohnwu.magisk.model.receiver.GeneralReceiver
-keepclassmembers class a.e { *; }
# Strip logging
-assumenosideeffects class timber.log.Timber.Tree { *; }
# Excessive obfuscation
-repackageclasses 'a'
-repackageclasses a
-allowaccessmodification
# QOL
......
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.topjohnwu.magisk">
......@@ -21,6 +20,10 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- Main -->
......
package a;
import com.topjohnwu.magisk.utils.PatchAPK;
import com.topjohnwu.magisk.core.utils.PatchAPK;
import com.topjohnwu.signing.BootSigner;
public class a {
......
package a;
import com.topjohnwu.magisk.ui.MainActivity;
public class b extends MainActivity {
/* stub */
}
package a;
import com.topjohnwu.magisk.ui.SplashActivity;
public class c extends SplashActivity {
/* stub */
}
package a;
import com.topjohnwu.magisk.App;
public class e extends App {
public e() {
super();
}
public e(Object o) {
super(o);
}
}
package a;
import com.topjohnwu.magisk.ui.flash.FlashActivity;
public class f extends FlashActivity {
/* stub */
}
......@@ -2,11 +2,11 @@ package a;
import android.content.Context;
import com.topjohnwu.magisk.model.update.UpdateCheckService;
import androidx.annotation.NonNull;
import androidx.work.WorkerParameters;
import com.topjohnwu.magisk.core.UpdateCheckService;
public class g extends w<UpdateCheckService> {
/* Stub */
public g(@NonNull Context context, @NonNull WorkerParameters workerParams) {
......
package a;
import com.topjohnwu.magisk.model.receiver.GeneralReceiver;
public class h extends GeneralReceiver {
/* stub */
}
package a;
import com.topjohnwu.magisk.model.download.DownloadService;
public class j extends DownloadService {
/* stub */
}
package a;
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity;
public class m extends SuRequestActivity {
/* stub */
}
package a
import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.topjohnwu.magisk.core.App
import com.topjohnwu.magisk.core.GeneralReceiver
import com.topjohnwu.magisk.core.SplashActivity
import com.topjohnwu.magisk.core.base.BaseWorkerWrapper
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.legacy.flash.FlashActivity
import com.topjohnwu.magisk.legacy.surequest.SuRequestActivity
import com.topjohnwu.magisk.ui.MainActivity
import java.lang.reflect.ParameterizedType
class b : MainActivity()
class c : SplashActivity()
class e : App {
constructor() : super()
constructor(o: Any) : super(o)
}
class f : FlashActivity()
class h : GeneralReceiver()
class j : DownloadService()
class m : SuRequestActivity()
/**
* Wrapper class to workaround Proguard rule :
* -keep class * extends Worker
* */
abstract class w<T : BaseWorkerWrapper>(
context: Context,
workerParams: WorkerParameters
) : Worker(context, workerParams) {
private var base: T? = null
override fun doWork() = base?.doWork() ?: Result.failure()
override fun onStopped() = base?.onStopped() ?: Unit
init {
try {
base = ((javaClass.genericSuperclass as ParameterizedType)
.actualTypeArguments[0] as Class<T>).newInstance()
base?.attachWorker(this)
} catch (e : java.lang.Exception) {}
}
}
package a;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.topjohnwu.magisk.base.DelegateWorker;
import java.lang.reflect.ParameterizedType;
public abstract class w<T extends DelegateWorker> extends Worker {
/* Wrapper class to workaround Proguard -keep class * extends Worker */
private T base;
@SuppressWarnings("unchecked")
w(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
try {
base = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass())
.getActualTypeArguments()[0]).newInstance();
base.attachWorker(this);
} catch (Exception ignored) {}
}
@NonNull
@Override
public Result doWork() {
if (base == null)
return Result.failure();
return base.doWork();
}
@Override
public void onStopped() {
if (base != null)
base.onStopped();
}
}
package com.topjohnwu.magisk.base
import android.annotation.SuppressLint
import android.content.SharedPreferences
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.*
import androidx.recyclerview.widget.RecyclerView
import org.koin.android.ext.android.inject
abstract class BasePreferenceFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
protected val prefs: SharedPreferences by inject()
protected val activity get() = requireActivity() as BaseActivity<*, *>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val v = super.onCreateView(inflater, container, savedInstanceState)
prefs.registerOnSharedPreferenceChangeListener(this)
return v
}
override fun onDestroyView() {
prefs.unregisterOnSharedPreferenceChangeListener(this)
super.onDestroyView()
}
private fun setAllPreferencesToAvoidHavingExtraSpace(preference: Preference) {
preference.isIconSpaceReserved = false
if (preference is PreferenceGroup)
for (i in 0 until preference.preferenceCount)
setAllPreferencesToAvoidHavingExtraSpace(preference.getPreference(i))
}
override fun setPreferenceScreen(preferenceScreen: PreferenceScreen?) {
if (preferenceScreen != null)
setAllPreferencesToAvoidHavingExtraSpace(preferenceScreen)
super.setPreferenceScreen(preferenceScreen)
}
override fun onCreateAdapter(preferenceScreen: PreferenceScreen?): RecyclerView.Adapter<*> =
object : PreferenceGroupAdapter(preferenceScreen) {
@SuppressLint("RestrictedApi")
override fun onPreferenceHierarchyChange(preference: Preference?) {
if (preference != null)
setAllPreferencesToAvoidHavingExtraSpace(preference)
super.onPreferenceHierarchyChange(preference)
}
}
}
package com.topjohnwu.magisk.base.viewmodel
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.utils.KObservableField
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import com.topjohnwu.magisk.Info.isConnected as gIsConnected
abstract class BaseViewModel(
initialState: State = State.LOADING
) : LoadingViewModel(initialState) {
val isConnected = object : KObservableField<Boolean>(gIsConnected.value, gIsConnected) {
override fun get(): Boolean {
return gIsConnected.value
}
}
fun withView(action: BaseActivity<*, *>.() -> Unit) {
ViewActionEvent(action).publish()
}
fun withPermissions(vararg permissions: String): Observable<Boolean> {
val subject = PublishSubject.create<Boolean>()
return subject.doOnSubscribeUi { PermissionEvent(permissions.toList(), subject).publish() }
}
fun back() = BackPressEvent().publish()
}
package com.topjohnwu.magisk.base.viewmodel
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import io.reactivex.*
abstract class LoadingViewModel(defaultState: State = State.LOADING) :
StatefulViewModel<LoadingViewModel.State>(defaultState) {
val loading @Bindable get() = state == State.LOADING
val loaded @Bindable get() = state == State.LOADED
val loadingFailed @Bindable get() = state == State.LOADING_FAILED
@Deprecated(
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
ReplaceWith("state = State.LOADING", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
DeprecationLevel.WARNING
)
fun setLoading() {
state = State.LOADING
}
@Deprecated(
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
ReplaceWith("state = State.LOADED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
DeprecationLevel.WARNING
)
fun setLoaded() {
state = State.LOADED
}
@Deprecated(
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
ReplaceWith("state = State.LOADING_FAILED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
DeprecationLevel.WARNING
)
fun setLoadingFailed() {
state = State.LOADING_FAILED
}
override fun notifyStateChanged() {
notifyPropertyChanged(BR.loading)
notifyPropertyChanged(BR.loaded)
notifyPropertyChanged(BR.loadingFailed)
}
enum class State {
LOADED, LOADING, LOADING_FAILED
}
//region Rx
protected fun <T> Observable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state = State.LOADING }
.doOnError { viewModel.state = State.LOADING_FAILED }
.doOnNext { if (allowFinishing) viewModel.state = State.LOADED }
protected fun <T> Single<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state = State.LOADING }
.doOnError { viewModel.state = State.LOADING_FAILED }
.doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED }
protected fun <T> Maybe<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state = State.LOADING }
.doOnError { viewModel.state = State.LOADING_FAILED }
.doOnComplete { if (allowFinishing) viewModel.state = State.LOADED }
.doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED }
protected fun <T> Flowable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state = State.LOADING }
.doOnError { viewModel.state = State.LOADING_FAILED }
.doOnNext { if (allowFinishing) viewModel.state = State.LOADED }
protected fun Completable.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state = State.LOADING }
.doOnError { viewModel.state = State.LOADING_FAILED }
.doOnComplete { if (allowFinishing) viewModel.state = State.LOADED }
//endregion
}
\ No newline at end of file
package com.topjohnwu.magisk.base.viewmodel
import androidx.databinding.Observable
import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.ViewModel
/**
* Copy of [android.databinding.BaseObservable] which extends [ViewModel]
*/
abstract class ObservableViewModel : TeanityViewModel(), Observable {
@Transient
private var callbacks: PropertyChangeRegistry? = null
@Synchronized
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
if (callbacks == null) {
callbacks = PropertyChangeRegistry()
}
callbacks?.add(callback)
}
@Synchronized
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
callbacks?.remove(callback)
}
/**
* Notifies listeners that all properties of this instance have changed.
*/
@Synchronized
fun notifyChange() {
callbacks?.notifyCallbacks(this, 0, null)
}
/**
* Notifies listeners that a specific property has changed. The getter for the property
* that changes should be marked with [android.databinding.Bindable] to generate a field in
* `BR` to be used as `fieldId`.
*
* @param fieldId The generated BR id for the Bindable field.
*/
fun notifyPropertyChanged(fieldId: Int) {
callbacks?.notifyCallbacks(this, fieldId, null)
}
}
\ No newline at end of file
package com.topjohnwu.magisk.base.viewmodel
abstract class StatefulViewModel<State : Enum<*>>(
val defaultState: State
) : ObservableViewModel() {
var state: State = defaultState
set(value) {
field = value
notifyStateChanged()
}
open fun notifyStateChanged() = Unit
}
\ No newline at end of file
package com.topjohnwu.magisk.base.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.topjohnwu.magisk.model.events.SimpleViewEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
abstract class TeanityViewModel : ViewModel() {
private val disposables = CompositeDisposable()
private val _viewEvents = MutableLiveData<ViewEvent>()
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
override fun onCleared() {
super.onCleared()
disposables.clear()
}
fun <Event : ViewEvent> Event.publish() {
_viewEvents.value = this
}
fun Int.publish() {
_viewEvents.value = SimpleViewEvent(this)
}
fun Disposable.add() {
disposables.add(this)
}
}
\ No newline at end of file
package com.topjohnwu.magisk
package com.topjohnwu.magisk.core
import android.app.Application
import android.content.Context
......@@ -9,6 +9,12 @@ import androidx.room.Room
import androidx.work.WorkManager
import androidx.work.impl.WorkDatabase
import androidx.work.impl.WorkDatabase_Impl
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.FileProvider
import com.topjohnwu.magisk.core.su.SuCallbackHandler
import com.topjohnwu.magisk.core.utils.RootInit
import com.topjohnwu.magisk.core.utils.updateConfig
import com.topjohnwu.magisk.data.database.RepoDatabase
import com.topjohnwu.magisk.data.database.RepoDatabase_Impl
import com.topjohnwu.magisk.data.database.SuLogDatabase
......@@ -17,13 +23,11 @@ import com.topjohnwu.magisk.di.ActivityTracker
import com.topjohnwu.magisk.di.koinModules
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.unwrap
import com.topjohnwu.magisk.utils.RootInit
import com.topjohnwu.magisk.utils.SuHandler
import com.topjohnwu.magisk.utils.updateConfig
import com.topjohnwu.superuser.Shell
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import timber.log.Timber
import kotlin.system.exitProcess
open class App() : Application() {
......@@ -37,7 +41,7 @@ open class App() : Application() {
Shell.Config.verboseLogging(BuildConfig.DEBUG)
Shell.Config.addInitializers(RootInit::class.java)
Shell.Config.setTimeout(2)
FileProvider.callHandler = SuHandler
FileProvider.callHandler = SuCallbackHandler
Room.setFactory {
when (it) {
WorkDatabase::class.java -> WorkDatabase_Impl()
......@@ -46,13 +50,19 @@ open class App() : Application() {
else -> null
}
}
// Always log full stack trace with Timber
Timber.plant(Timber.DebugTree())
Thread.setDefaultUncaughtExceptionHandler { _, e ->
Timber.e(e)
exitProcess(1)
}
}
override fun attachBaseContext(base: Context) {
// Basic setup
if (BuildConfig.DEBUG)
MultiDex.install(base)
Timber.plant(Timber.DebugTree())
// Some context magic
val app: Application
......
package com.topjohnwu.magisk
package com.topjohnwu.magisk.core
import android.content.Context
import android.content.SharedPreferences
import android.os.Environment
import android.util.Xml
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import com.topjohnwu.magisk.data.database.SettingsDao
import com.topjohnwu.magisk.data.database.StringDao
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
import com.topjohnwu.magisk.core.magiskdb.StringDao
import com.topjohnwu.magisk.data.repository.DBConfig
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.model.preference.PreferenceModel
import com.topjohnwu.magisk.utils.BiometricHelper
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.magisk.core.utils.BiometricHelper
import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
import com.topjohnwu.superuser.io.SuFileInputStream
......@@ -45,10 +48,15 @@ object Config : PreferenceModel, DBConfig {
const val CUSTOM_CHANNEL = "custom_channel"
const val LOCALE = "locale"
const val DARK_THEME = "dark_theme"
const val DARK_THEME_EXTENDED = "dark_theme_extended"
const val REPO_ORDER = "repo_order"
const val SHOW_SYSTEM_APP = "show_system"
const val DOWNLOAD_PATH = "download_path"
const val REDESIGN = "redesign"
const val SAFETY = "safety_notice"
const val THEME_ORDINAL = "theme_ordinal"
const val BOOT_ID = "boot_id"
const val LIST_SPAN_COUNT = "list_span_count"
// system state
const val MAGISKHIDE = "magiskhide"
......@@ -103,39 +111,67 @@ object Config : PreferenceModel, DBConfig {
Value.CANARY_DEBUG_CHANNEL
else
Value.CANARY_CHANNEL
}
else Value.DEFAULT_CHANNEL
} else Value.DEFAULT_CHANNEL
var bootId by preference(Key.BOOT_ID, "")
var downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS)
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
var repoOrder by preference(
Key.REPO_ORDER,
Value.ORDER_DATE
)
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
var suAutoReponse by preferenceStrInt(
Key.SU_AUTO_RESPONSE,
Value.SU_PROMPT
)
var suNotification by preferenceStrInt(
Key.SU_NOTIFICATION,
Value.NOTIFICATION_TOAST
)
var updateChannel by preferenceStrInt(
Key.UPDATE_CHANNEL,
defaultChannel
)
var darkTheme by preference(Key.DARK_THEME, true)
var safetyNotice by preference(Key.SAFETY, true)
var darkThemeExtended by preference(
Key.DARK_THEME_EXTENDED,
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
)
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
var suReAuth by preference(Key.SU_REAUTH, false)
var checkUpdate by preference(Key.CHECK_UPDATES, true)
var magiskHide by preference(Key.MAGISKHIDE, true)
@JvmStatic
var coreOnly by preference(Key.COREONLY, false)
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
var listSpanCount by preference(Key.LIST_SPAN_COUNT, 2)
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
var locale by preference(Key.LOCALE, "")
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)
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
)
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
var suManager by dbStrings(Key.SU_MANAGER, "", true)
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
// Always return a path in external storage where we can write
val downloadDirectory get() =
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
val downloadDirectory
get() =
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
private const val SU_FINGERPRINT = "su_fingerprint"
......@@ -163,7 +199,9 @@ object Config : PreferenceModel, DBConfig {
}
private fun parsePrefs(editor: SharedPreferences.Editor) = editor.apply {
val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS)
val config = SuFile.open("/data/adb",
Const.MANAGER_CONFIGS
)
if (config.exists()) runCatching {
val input = SuFileInputStream(config)
val parser = Xml.newPullParser()
......
package com.topjohnwu.magisk
package com.topjohnwu.magisk.core
import android.os.Process
import java.io.File
......@@ -62,6 +62,7 @@ object Const {
const val ETAG_KEY = "ETag"
// intents
const val OPEN_SECTION = "section"
const val OPEN_SETTINGS = "settings"
const val INTENT_SET_APP = "app_json"
const val FLASH_ACTION = "action"
const val FLASH_DATA = "additional_data"
......
package com.topjohnwu.magisk.model.receiver
package com.topjohnwu.magisk.core
import android.content.ContextWrapper
import android.content.Intent
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.base.BaseReceiver
import com.topjohnwu.magisk.data.database.PolicyDao
import com.topjohnwu.magisk.core.base.BaseReceiver
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.model.ManagerJson
import com.topjohnwu.magisk.core.su.SuCallbackHandler
import com.topjohnwu.magisk.core.view.Shortcuts
import com.topjohnwu.magisk.extensions.reboot
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.ManagerJson
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.utils.SuHandler
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
import org.koin.core.inject
......@@ -30,7 +27,7 @@ open class GeneralReceiver : BaseReceiver() {
when (intent.action ?: return) {
Intent.ACTION_REBOOT -> {
SuHandler(context, intent.getStringExtra("action"), intent.extras)
SuCallbackHandler(context, intent.getStringExtra("action"), intent.extras)
}
Intent.ACTION_PACKAGE_REPLACED -> {
// This will only work pre-O
......
@file:Suppress("DEPRECATION")
package com.topjohnwu.magisk
package com.topjohnwu.magisk.core
import android.annotation.SuppressLint
import android.app.job.JobInfo
......@@ -14,23 +14,22 @@ import android.content.res.AssetManager
import android.content.res.Configuration
import android.content.res.Resources
import androidx.annotation.RequiresApi
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.ProcessPhoenix
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.utils.refreshLocale
import com.topjohnwu.magisk.core.utils.updateConfig
import com.topjohnwu.magisk.extensions.forceGetDeclaredField
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.model.update.UpdateCheckService
import com.topjohnwu.magisk.legacy.flash.FlashActivity
import com.topjohnwu.magisk.legacy.surequest.SuRequestActivity
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.ui.SplashActivity
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.magisk.utils.refreshLocale
import com.topjohnwu.magisk.utils.updateConfig
fun AssetManager.addAssetPath(path: String) {
DynAPK.addAssetPath(this, path)
}
fun Context.wrap(global: Boolean = true): Context
= if (global) GlobalResContext(this) else ResContext(this)
fun Context.wrap(global: Boolean = true): Context =
if (global) GlobalResContext(this) else ResContext(this)
fun Context.wrapJob(): Context = object : GlobalResContext(this) {
......@@ -130,7 +129,8 @@ private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler
val name = service.className
val component = ComponentName(
service.packageName,
Info.stub!!.classToComponent[name] ?: name)
Info.stub!!.classToComponent[name] ?: name
)
javaClass.forceGetDeclaredField("service")?.set(this, component)
return this
......
package com.topjohnwu.magisk
package com.topjohnwu.magisk.core
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.core.model.UpdateInfo
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.UpdateInfo
import com.topjohnwu.magisk.utils.CachedValue
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.superuser.Shell
......@@ -17,12 +18,16 @@ object Info {
val envRef = CachedValue { loadState() }
@JvmStatic
val env by envRef // Local
var remote = UpdateInfo() // Remote
var stub: DynAPK.Data? = null // Stub
@JvmStatic
var keepVerity = false
@JvmStatic
var keepEnc = false
@JvmStatic
var recovery = false
val isConnected by lazy {
......
package com.topjohnwu.magisk.ui
package com.topjohnwu.magisk.core
import android.app.Activity
import android.content.Context
import android.os.Bundle
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.magisk.core.view.Notifications
import com.topjohnwu.magisk.core.view.Shortcuts
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
......@@ -61,8 +62,7 @@ open class SplashActivity : Activity() {
}
DONE = true
startActivity(intent<MainActivity>().apply { intent?.also { putExtras(it) } })
Navigation.start(intent, this)
finish()
}
......
package com.topjohnwu.magisk.model.update
package com.topjohnwu.magisk.core
import androidx.work.ListenableWorker
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.base.DelegateWorker
import com.topjohnwu.magisk.core.base.BaseWorkerWrapper
import com.topjohnwu.magisk.core.view.Notifications
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.Shell
class UpdateCheckService : DelegateWorker() {
class UpdateCheckService : BaseWorkerWrapper() {
private val magiskRepo: MagiskRepository by inject()
......
package com.topjohnwu.magisk.base
package com.topjohnwu.magisk.core.base
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.SparseArrayCompat
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.core.wrap
import com.topjohnwu.magisk.extensions.set
import com.topjohnwu.magisk.model.events.EventHandler
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
import com.topjohnwu.magisk.utils.currentLocale
import com.topjohnwu.magisk.wrap
import kotlin.random.Random
typealias RequestCallback = BaseActivity<*, *>.(Int, Intent?) -> Unit
typealias RequestCallback = BaseActivity.(Int, Intent?) -> Unit
abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
AppCompatActivity(), EventHandler {
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
protected abstract val viewModel: ViewModel
protected open val themeRes: Int = R.style.MagiskTheme
protected open val snackbarView get() = binding.root
abstract class BaseActivity : AppCompatActivity() {
private val resultCallbacks by lazy { SparseArrayCompat<RequestCallback>() }
init {
val theme = if (Config.darkTheme) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
}
AppCompatDelegate.setDefaultNightMode(theme)
}
override fun applyOverrideConfiguration(config: Configuration?) {
// Force applying our preferred local
config?.setLocale(currentLocale)
......@@ -56,18 +31,6 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding
super.attachBaseContext(base.wrap(false))
}
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(themeRes)
super.onCreate(savedInstanceState)
viewModel.viewEvents.observe(this, viewEventObserver)
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).apply {
setVariable(BR.viewModel, viewModel)
lifecycleOwner = this@BaseActivity
}
}
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
val request = PermissionRequestBuilder().apply(builder).build()
val ungranted = permissions.filter {
......@@ -93,7 +56,7 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
var success = true
for (res in grantResults) {
if (res != PackageManager.PERMISSION_GRANTED) {
......@@ -101,18 +64,18 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding
break
}
}
resultCallbacks[requestCode]?.apply {
resultCallbacks[requestCode]?.also {
resultCallbacks.remove(requestCode)
invoke(this@BaseActivity, if (success) 1 else -1, null)
it(this@BaseActivity, if (success) 1 else -1, null)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
resultCallbacks[requestCode]?.apply {
resultCallbacks[requestCode]?.also {
resultCallbacks.remove(requestCode)
invoke(this@BaseActivity, resultCode, data)
it(this@BaseActivity, resultCode, data)
}
}
......
package com.topjohnwu.magisk.base
package com.topjohnwu.magisk.core.base
import android.content.BroadcastReceiver
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import com.topjohnwu.magisk.wrap
import com.topjohnwu.magisk.core.wrap
import org.koin.core.KoinComponent
abstract class BaseReceiver : BroadcastReceiver(), KoinComponent {
......
package com.topjohnwu.magisk.base
package com.topjohnwu.magisk.core.base
import android.app.Service
import android.content.Context
import com.topjohnwu.magisk.wrap
import com.topjohnwu.magisk.core.wrap
import org.koin.core.KoinComponent
abstract class BaseService : Service(), KoinComponent {
......
package com.topjohnwu.magisk.base
package com.topjohnwu.magisk.core.base
import android.content.Context
import android.net.Network
......@@ -10,7 +10,7 @@ import androidx.work.ListenableWorker
import com.google.common.util.concurrent.ListenableFuture
import java.util.*
abstract class DelegateWorker {
abstract class BaseWorkerWrapper {
private lateinit var worker: ListenableWorker
......
package com.topjohnwu.magisk.model.download
package com.topjohnwu.magisk.core.download
import android.annotation.SuppressLint
import android.app.Notification
......@@ -8,15 +8,15 @@ import android.content.Intent
import android.os.Build
import android.webkit.MimeTypeMap
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.extensions.chooser
import com.topjohnwu.magisk.extensions.exists
import com.topjohnwu.magisk.extensions.provide
import com.topjohnwu.magisk.intent
import com.topjohnwu.magisk.legacy.flash.FlashActivity
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.magisk.utils.APKInstall
import org.koin.core.get
import java.io.File
......@@ -114,13 +114,15 @@ open class DownloadService : RemoteFileService() {
@Suppress("ReplaceSingleLineLet")
private fun Notification.Builder.setContentIntent(intent: Intent) =
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
.let { setContentIntent(it) }
setContentIntent(
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
)
@Suppress("ReplaceSingleLineLet")
private fun Notification.Builder.addAction(icon: Int, title: Int, intent: Intent) =
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
.let { addAction(icon, getString(title), it) }
addAction(icon, getString(title),
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
)
// ---
......
package com.topjohnwu.magisk.model.download
package com.topjohnwu.magisk.core.download
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.ProcessPhoenix
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.utils.PatchAPK
import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Restore
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Upgrade
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.utils.PatchAPK
import com.topjohnwu.superuser.Shell
import java.io.File
......
package com.topjohnwu.magisk.model.download
package com.topjohnwu.magisk.core.download
import com.topjohnwu.magisk.extensions.withStreams
import java.io.File
......@@ -41,4 +41,4 @@ fun InputStream.toModule(file: File, installer: InputStream) {
entry = zin.nextEntry
}
}
}
\ No newline at end of file
}
package com.topjohnwu.magisk.model.download
package com.topjohnwu.magisk.core.download
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import com.topjohnwu.magisk.base.BaseService
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.view.Notifications
import org.koin.core.KoinComponent
import java.util.*
import kotlin.random.Random.Default.nextInt
......
package com.topjohnwu.magisk.model.download
package com.topjohnwu.magisk.core.download
import android.app.Activity
import android.app.Notification
import android.content.Intent
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.di.NullActivity
......@@ -11,12 +13,13 @@ import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
import com.topjohnwu.magisk.utils.ProgressInputStream
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.core.view.Notifications
import com.topjohnwu.superuser.ShellUtils
import io.reactivex.Completable
import okhttp3.ResponseBody
import org.koin.android.ext.android.inject
import org.koin.core.KoinComponent
import timber.log.Timber
import java.io.InputStream
......@@ -39,11 +42,7 @@ abstract class RemoteFileService : NotificationService() {
.doOnSubscribe { update(subject.hashCode()) { it.setContentTitle(subject.title) } }
.subscribeK(onError = {
Timber.e(it)
finishNotify(subject.hashCode()) { notification ->
notification.setContentText(getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
}
failNotify(subject)
}) {
val newId = finishNotify(subject)
if (get<Activity>() !is NullActivity) {
......@@ -62,12 +61,12 @@ abstract class RemoteFileService : NotificationService() {
}
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
.map { it.toStream(subject.hashCode()) }
.map { it.toStream(subject.hashCode(), subject) }
.flatMapCompletable { stream ->
when (subject) {
is Module -> service.fetchInstaller()
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
.ignoreElement()
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
.ignoreElement()
else -> Completable.fromAction { stream.writeTo(subject.file) }
}
}.doOnComplete {
......@@ -75,7 +74,7 @@ abstract class RemoteFileService : NotificationService() {
handleAPK(subject)
}
private fun ResponseBody.toStream(id: Int): InputStream {
private fun ResponseBody.toStream(id: Int, subject: DownloadSubject): InputStream {
val maxRaw = contentLength()
val max = maxRaw / 1_000_000f
......@@ -83,17 +82,27 @@ abstract class RemoteFileService : NotificationService() {
val progress = it / 1_000_000f
update(id) { notification ->
if (maxRaw > 0) {
send(progress / max, subject)
notification
.setProgress(maxRaw.toInt(), it.toInt(), false)
.setContentText("%.2f / %.2f MB".format(progress, max))
.setProgress(maxRaw.toInt(), it.toInt(), false)
.setContentText("%.2f / %.2f MB".format(progress, max))
} else {
send(-1f, subject)
notification.setContentText("%.2f MB / ??".format(progress))
}
}
}
}
private fun failNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) {
send(0f, subject)
it.setContentText(getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
}
private fun finishNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) {
send(1f, subject)
it.addActions(subject)
.setContentText(getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
......@@ -111,8 +120,19 @@ abstract class RemoteFileService : NotificationService() {
protected abstract fun Notification.Builder.addActions(subject: DownloadSubject)
: Notification.Builder
companion object {
companion object : KoinComponent {
const val ARG_URL = "arg_url"
private val internalProgressBroadcast = MutableLiveData<Pair<Float, DownloadSubject>>()
val progressBroadcast: LiveData<Pair<Float, DownloadSubject>> get() = internalProgressBroadcast
fun send(progress: Float, subject: DownloadSubject) {
internalProgressBroadcast.postValue(progress to subject)
}
fun reset() {
internalProgressBroadcast.value = null
}
}
}
package com.topjohnwu.magisk.data.database.magiskdb
package com.topjohnwu.magisk.core.magiskdb
import androidx.annotation.StringDef
import com.topjohnwu.superuser.Shell
......
package com.topjohnwu.magisk.data.database
package com.topjohnwu.magisk.core.magiskdb
import android.content.Context
import android.content.pm.PackageManager
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.database.magiskdb.BaseDao
import com.topjohnwu.magisk.data.database.magiskdb.Delete
import com.topjohnwu.magisk.data.database.magiskdb.Replace
import com.topjohnwu.magisk.data.database.magiskdb.Select
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.model.MagiskPolicy
import com.topjohnwu.magisk.core.model.toMap
import com.topjohnwu.magisk.core.model.toPolicy
import com.topjohnwu.magisk.extensions.now
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.toMap
import com.topjohnwu.magisk.model.entity.toPolicy
import timber.log.Timber
import java.util.concurrent.TimeUnit
......
package com.topjohnwu.magisk.data.database.magiskdb
package com.topjohnwu.magisk.core.magiskdb
import androidx.annotation.StringDef
......
package com.topjohnwu.magisk.data.database
import com.topjohnwu.magisk.data.database.magiskdb.BaseDao
import com.topjohnwu.magisk.data.database.magiskdb.Delete
import com.topjohnwu.magisk.data.database.magiskdb.Replace
import com.topjohnwu.magisk.data.database.magiskdb.Select
package com.topjohnwu.magisk.core.magiskdb
class SettingsDao : BaseDao() {
......
package com.topjohnwu.magisk.data.database
import com.topjohnwu.magisk.data.database.magiskdb.BaseDao
import com.topjohnwu.magisk.data.database.magiskdb.Delete
import com.topjohnwu.magisk.data.database.magiskdb.Replace
import com.topjohnwu.magisk.data.database.magiskdb.Select
package com.topjohnwu.magisk.core.magiskdb
class StringDao : BaseDao() {
......
package com.topjohnwu.magisk.model.entity
package com.topjohnwu.magisk.core.model
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.INTERACTIVE
import com.topjohnwu.magisk.extensions.getLabel
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE
data class MagiskPolicy(
......
package com.topjohnwu.magisk.model.entity
package com.topjohnwu.magisk.core.model
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
......
package com.topjohnwu.magisk.model.entity.module
package com.topjohnwu.magisk.core.model.module
abstract class BaseModule : Comparable<BaseModule> {
abstract var id: String
......
package com.topjohnwu.magisk.model.entity.module
package com.topjohnwu.magisk.core.model.module
import androidx.annotation.WorkerThread
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
......
package com.topjohnwu.magisk.model.entity.module
package com.topjohnwu.magisk.core.model.module
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.legalFilename
......
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.core.su
import android.content.Context
import android.content.Intent
......@@ -6,20 +6,26 @@ import android.os.Build
import android.os.Bundle
import android.os.Process
import android.widget.Toast
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.ProviderCallHandler
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.model.MagiskPolicy
import com.topjohnwu.magisk.core.model.toPolicy
import com.topjohnwu.magisk.core.wrap
import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.startActivity
import com.topjohnwu.magisk.extensions.startActivityWithRoot
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.legacy.surequest.SuRequestActivity
import com.topjohnwu.magisk.model.entity.toLog
import com.topjohnwu.magisk.model.entity.toPolicy
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.superuser.Shell
import timber.log.Timber
object SuHandler : ProviderCallHandler {
object SuCallbackHandler : ProviderCallHandler {
const val REQUEST = "request"
const val LOG = "log"
......
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.core.su
import android.net.LocalSocket
import android.net.LocalSocketAddress
......
package com.topjohnwu.magisk.core.su
import android.content.Intent
import android.content.pm.PackageManager
import android.os.CountDownTimer
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.model.MagiskPolicy
import com.topjohnwu.magisk.core.model.toPolicy
import com.topjohnwu.magisk.extensions.now
import timber.log.Timber
import java.util.concurrent.TimeUnit
abstract class SuRequestHandler(
private val packageManager: PackageManager,
private val policyDB: PolicyDao
) {
protected var timer: CountDownTimer = object : CountDownTimer(
TimeUnit.MINUTES.toMillis(1), TimeUnit.MINUTES.toMillis(1)) {
override fun onFinish() {
respond(MagiskPolicy.DENY, 0)
}
override fun onTick(remains: Long) {}
}
set(value) {
field.cancel()
field = value
field.start()
}
protected lateinit var policy: MagiskPolicy
private val cleanupTasks = mutableListOf<() -> Unit>()
private lateinit var connector: SuConnector
abstract fun onStart()
abstract fun onRespond()
fun start(intent: Intent): Boolean {
val socketName = intent.getStringExtra("socket") ?: return false
try {
connector = object : SuConnector(socketName) {
override fun onResponse() {
out.writeInt(policy.policy)
}
}
val map = connector.readRequest()
val uid = map["uid"]?.toIntOrNull() ?: return false
policy = uid.toPolicy(packageManager)
} catch (e: Exception) {
Timber.e(e)
return false
}
// Never allow com.topjohnwu.magisk (could be malware)
if (policy.packageName == BuildConfig.APPLICATION_ID)
return false
when (Config.suAutoReponse) {
Config.Value.SU_AUTO_DENY -> {
respond(MagiskPolicy.DENY, 0)
return true
}
Config.Value.SU_AUTO_ALLOW -> {
respond(MagiskPolicy.ALLOW, 0)
return true
}
}
timer.start()
cleanupTasks.add {
timer.cancel()
}
onStart()
return true
}
private fun respond() {
connector.response()
cleanupTasks.forEach { it() }
onRespond()
}
fun respond(action: Int, time: Int) {
val until = if (time > 0)
TimeUnit.MILLISECONDS.toSeconds(now) + TimeUnit.MINUTES.toSeconds(time.toLong())
else
time.toLong()
policy.policy = action
policy.until = until
policy.uid = policy.uid % 100000 + Const.USER_ID * 100000
if (until >= 0)
policyDB.update(policy).blockingAwait()
respond()
}
}
package com.topjohnwu.magisk.tasks
package com.topjohnwu.magisk.core.tasks
import android.content.Context
import android.net.Uri
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.extensions.fileName
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.readUri
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.utils.unzip
import com.topjohnwu.magisk.core.utils.unzip
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
import java.io.File
......
package com.topjohnwu.magisk.tasks
package com.topjohnwu.magisk.core.tasks
import android.content.Context
import android.net.Uri
......@@ -6,11 +6,13 @@ import android.os.Build
import android.text.TextUtils
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Info
import androidx.core.net.toUri
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.net.Networking
import com.topjohnwu.signing.SignBoot
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
......@@ -34,10 +36,10 @@ import java.util.zip.ZipInputStream
abstract class MagiskInstaller {
protected lateinit var srcBoot: String
protected lateinit var destFile: File
protected lateinit var installDir: File
protected lateinit var zipUri: Uri
private lateinit var srcBoot: String
private lateinit var destFile: File
private lateinit var zipUri: Uri
private val console: MutableList<String>
private val logs: MutableList<String>
......@@ -60,7 +62,7 @@ abstract class MagiskInstaller {
installDir.mkdirs()
}
protected fun findImage(): Boolean {
private fun findImage(): Boolean {
srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
if (srcBoot.isEmpty()) {
console.add("! Unable to detect target image")
......@@ -70,7 +72,7 @@ abstract class MagiskInstaller {
return true
}
protected fun findSecondaryImage(): Boolean {
private fun findSecondaryImage(): Boolean {
val slot = "echo \$SLOT".fsh()
val target = if (slot == "_a") "_b" else "_a"
console.add("- Target slot: $target")
......@@ -87,7 +89,7 @@ abstract class MagiskInstaller {
return true
}
protected fun extractZip(): Boolean {
private fun extractZip(): Boolean {
val arch: String
arch = if (Build.VERSION.SDK_INT >= 21) {
val abis = listOf(*Build.SUPPORTED_ABIS)
......@@ -208,7 +210,7 @@ abstract class MagiskInstaller {
}
}
protected fun handleFile(uri: Uri): Boolean {
private fun handleFile(uri: Uri): Boolean {
try {
context.readUri(uri).buffered().use {
it.mark(500)
......@@ -238,7 +240,7 @@ abstract class MagiskInstaller {
return true
}
protected fun patchBoot(): Boolean {
private fun patchBoot(): Boolean {
var isSigned = false
try {
SuFileInputStream(srcBoot).use {
......@@ -284,7 +286,7 @@ abstract class MagiskInstaller {
return true
}
protected fun flashBoot(): Boolean {
private fun flashBoot(): Boolean {
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
return false
arrayOf(
......@@ -294,7 +296,7 @@ abstract class MagiskInstaller {
return true
}
protected fun storeBoot(): Boolean {
private fun storeBoot(): Boolean {
val patched = SuFile.open(installDir, "new-boot.img")
try {
val os = tarOut?.let {
......@@ -320,7 +322,7 @@ abstract class MagiskInstaller {
return true
}
protected fun postOTA(): Boolean {
private fun postOTA(): Boolean {
val bootctl = SuFile("/data/adb/bootctl")
try {
withStreams(service.fetchBootctl().blockingGet().byteStream(), bootctl.suOutputStream()) {
......@@ -345,6 +347,28 @@ abstract class MagiskInstaller {
private fun String.fsh() = ShellUtils.fastCmd(this)
private fun Array<String>.fsh() = ShellUtils.fastCmd(*this)
protected fun doPatchFile(patchFile: Uri) =
extractZip() && handleFile(patchFile) && patchBoot() && storeBoot()
protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot()
protected fun secondSlot() =
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
protected fun fixEnv(): Boolean {
val context = get<Context>()
val zip: File = context.cachedFile("magisk.zip")
installDir = SuFile("/data/adb/magisk")
Shell.su("rm -rf /data/adb/magisk/*").exec()
if (!ShellUtils.checkSum("MD5", zip, Info.remote.magisk.md5))
Networking.get(Info.remote.magisk.link).execForFile(zip)
zipUri = zip.toUri()
return extractZip() && Shell.su("fix_env").exec().isSuccess
}
@WorkerThread
protected abstract fun operations(): Boolean
......
package com.topjohnwu.magisk.tasks
package com.topjohnwu.magisk.core.tasks
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.data.network.GithubApiServices
import com.topjohnwu.magisk.model.entity.module.Repo
import io.reactivex.Completable
import io.reactivex.Flowable
import io.reactivex.Single
import io.reactivex.rxkotlin.toFlowable
import io.reactivex.schedulers.Schedulers
import se.ansman.kotshi.JsonSerializable
......@@ -19,18 +19,17 @@ class RepoUpdater(
private val api: GithubApiServices,
private val repoDB: RepoDao
) {
private fun loadRepos(repos: List<GithubRepoInfo>, cached: MutableSet<String>) =
repos.toFlowable().parallel().runOn(Schedulers.io()).map {
// Skip submission
if (it.id == "submission")
return@map
(repoDB.getRepo(it.id)?.apply { cached.remove(it.id) } ?:
Repo(it.id)).runCatching {
update(it.pushDate)
repoDB.addRepo(this)
}.getOrElse { Timber.e(it) }
}.sequential()
repos.toFlowable().parallel().runOn(Schedulers.io()).map {
// Skip submission
if (it.id == "submission")
return@map
val repo = repoDB.getRepo(it.id)?.apply { cached.remove(it.id) } ?: Repo(it.id)
repo.runCatching {
update(it.pushDate)
repoDB.addRepo(this)
}.getOrElse(Timber::e)
}.sequential()
private fun loadPage(
cached: MutableSet<String>,
......@@ -40,7 +39,7 @@ class RepoUpdater(
it.error()?.also { throw it }
it.response()?.run {
if (code() == HttpURLConnection.HTTP_NOT_MODIFIED)
return@run Flowable.error<Unit>(CachedException)
return@run Flowable.error<Unit>(CachedException())
if (page == 1)
repoDB.etagKey = headers()[Const.Key.ETAG_KEY].orEmpty().trimEtag()
......@@ -55,36 +54,40 @@ class RepoUpdater(
}
private fun forcedReload(cached: MutableSet<String>) =
cached.toFlowable().parallel().runOn(Schedulers.io()).map {
runCatching {
Repo(it).update()
}.getOrElse { Timber.e(it) }
}.sequential()
cached.toFlowable().parallel().runOn(Schedulers.io()).map {
runCatching {
Repo(it).update()
}.getOrElse(Timber::e)
}.sequential()
private fun String.trimEtag() = substring(indexOf('\"'), lastIndexOf('\"') + 1)
operator fun invoke(forced: Boolean = false) : Single<Unit> {
val cached = Collections.synchronizedSet(HashSet(repoDB.repoIDList))
return loadPage(cached, etag = repoDB.etagKey).doOnComplete {
repoDB.removeRepos(cached)
}.onErrorResumeNext { it: Throwable ->
if (it is CachedException) {
if (forced)
return@onErrorResumeNext forcedReload(cached)
} else {
Timber.e(it)
}
Flowable.empty()
}.ignoreElements().toSingleDefault(Unit)
@Suppress("RedundantLambdaArrow")
operator fun invoke(forced: Boolean) : Completable {
return Flowable
.fromCallable { Collections.synchronizedSet(HashSet(repoDB.repoIDList)) }
.flatMap { cached ->
loadPage(cached, etag = repoDB.etagKey).doOnComplete {
repoDB.removeRepos(cached)
}.onErrorResumeNext { it: Throwable ->
if (it is CachedException) {
if (forced)
return@onErrorResumeNext forcedReload(cached)
} else {
Timber.e(it)
}
Flowable.empty()
}
}.ignoreElements()
}
object CachedException : Exception()
class CachedException : Exception()
}
private val dateFormat: SimpleDateFormat =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
@JsonSerializable
data class GithubRepoInfo(
......
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.core.utils
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import org.koin.core.KoinComponent
import org.koin.core.get
......
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.core.utils
import android.content.pm.PackageManager
import android.util.Base64
import android.util.Base64OutputStream
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.utils.PatchAPK.ALPHANUM
import com.topjohnwu.magisk.di.koinModules
import com.topjohnwu.magisk.utils.PatchAPK.ALPHANUM
import com.topjohnwu.signing.CryptoUtils.readCertificate
import com.topjohnwu.signing.CryptoUtils.readPrivateKey
import com.topjohnwu.superuser.internal.InternalUtils
......@@ -50,10 +50,14 @@ class Keygen: CertKeyProvider {
private val provider: CertKeyProvider
inner class KeyStoreProvider : CertKeyProvider {
inner class KeyStoreProvider :
CertKeyProvider {
private val ks by lazy { init() }
override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate }
override val key by lazy { ks.getKey(ALIAS, PASSWORD) as PrivateKey }
override val key by lazy { ks.getKey(
ALIAS,
PASSWORD
) as PrivateKey }
}
class TestProvider : CertKeyProvider {
......@@ -113,8 +117,12 @@ class Keygen: CertKeyProvider {
if (raw.isEmpty()) {
ks.load(null)
} else {
GZIPInputStream(Base64.decode(raw, BASE64_FLAG).inputStream()).use {
ks.load(it, PASSWORD)
GZIPInputStream(Base64.decode(raw,
BASE64_FLAG
).inputStream()).use {
ks.load(it,
PASSWORD
)
}
}
......@@ -131,10 +139,16 @@ class Keygen: CertKeyProvider {
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
// Store them into keystore
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert))
ks.setKeyEntry(
ALIAS, kp.private,
PASSWORD, arrayOf(cert))
val bytes = ByteArrayOutputStream()
GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use {
ks.store(it, PASSWORD)
GZIPOutputStream(Base64OutputStream(bytes,
BASE64_FLAG
)).use {
ks.store(it,
PASSWORD
)
}
Config.keyStoreRaw = bytes.toString("UTF-8")
......
@file:Suppress("DEPRECATION")
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.core.utils
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.content.res.Resources
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.ResourceMgr
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.ResourceMgr
import com.topjohnwu.magisk.extensions.langTagToLocale
import com.topjohnwu.magisk.extensions.toLangTag
import io.reactivex.Single
......
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.core.utils
import android.content.Context
import android.os.Build.VERSION.SDK_INT
import android.widget.Toast
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.extensions.DynamicClassLoader
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.core.view.Notifications
import com.topjohnwu.signing.JarMap
import com.topjohnwu.signing.SignAPK
import com.topjohnwu.superuser.Shell
......
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.core.utils
import com.topjohnwu.superuser.internal.UiThreadHandler
import java.io.FilterInputStream
......@@ -41,4 +41,4 @@ class ProgressInputStream(
}
return sz
}
}
\ No newline at end of file
}
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.core.utils
import android.content.Context
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.wrap
import com.topjohnwu.magisk.extensions.rawResource
import com.topjohnwu.magisk.wrap
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
......
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.core.utils
interface SafetyNetHelper {
......@@ -12,10 +12,10 @@ interface SafetyNetHelper {
companion object {
val RESPONSE_ERR = 0x01
val CONNECTION_FAIL = 0x02
const val RESPONSE_ERR = 0x01
const val CONNECTION_FAIL = 0x02
val BASIC_PASS = 0x10
val CTS_PASS = 0x20
const val BASIC_PASS = 0x10
const val CTS_PASS = 0x20
}
}
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.core.utils
import android.content.Context
import android.content.Intent
......@@ -7,10 +7,10 @@ import android.net.Uri
import android.os.Environment
import android.widget.Toast
import androidx.work.*
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.model.update.UpdateCheckService
import com.topjohnwu.superuser.internal.UiThreadHandler
import java.io.File
import java.util.concurrent.TimeUnit
......@@ -62,7 +62,10 @@ object Utils {
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
toast(R.string.open_link_failed_toast, Toast.LENGTH_SHORT)
toast(
R.string.open_link_failed_toast,
Toast.LENGTH_SHORT
)
}
}
......
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.core.utils
import com.topjohnwu.superuser.io.SuFile
import com.topjohnwu.superuser.io.SuFileOutputStream
......
package com.topjohnwu.magisk.view
package com.topjohnwu.magisk.core.view
import android.app.Notification
import android.app.NotificationChannel
......@@ -9,13 +9,12 @@ import android.os.Build.VERSION.SDK_INT
import androidx.core.app.TaskStackBuilder
import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toIcon
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.Const.ID.UPDATE_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.getBitmap
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.ui.SplashActivity
object Notifications {
......@@ -52,10 +51,13 @@ object Notifications {
val stackBuilder = TaskStackBuilder.create(context)
stackBuilder.addParentStack(SplashActivity::class.java.cmp(context.packageName))
stackBuilder.addNextIntent(intent)
val pendingIntent = stackBuilder.getPendingIntent(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID,
val pendingIntent = stackBuilder.getPendingIntent(
Const.ID.MAGISK_UPDATE_NOTIFICATION_ID,
PendingIntent.FLAG_UPDATE_CURRENT)
val builder = updateBuilder(context)
val builder = updateBuilder(
context
)
.setContentTitle(context.getString(R.string.magisk_update_title))
.setContentText(context.getString(R.string.manager_download_install))
.setAutoCancel(true)
......@@ -72,7 +74,9 @@ object Notifications {
val pendingIntent = PendingIntent.getBroadcast(context,
Const.ID.APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val builder = updateBuilder(context)
val builder = updateBuilder(
context
)
.setContentTitle(context.getString(R.string.manager_update_title))
.setContentText(context.getString(R.string.manager_download_install))
.setAutoCancel(true)
......@@ -87,7 +91,9 @@ object Notifications {
val pendingIntent = PendingIntent.getBroadcast(context,
Const.ID.DTBO_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val builder = updateBuilder(context)
val builder = updateBuilder(
context
)
.setContentTitle(context.getString(R.string.dtbo_patched_title))
.setContentText(context.getString(R.string.dtbo_patched_reboot))
......
package com.topjohnwu.magisk.view
package com.topjohnwu.magisk.core.view
import android.content.Context
import android.content.Intent
......@@ -10,17 +10,18 @@ import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toAdaptiveIcon
import androidx.core.graphics.drawable.toIcon
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.magisk.extensions.getBitmap
import com.topjohnwu.magisk.ui.SplashActivity
import com.topjohnwu.magisk.utils.Utils
object Shortcuts {
fun setup(context: Context) {
if (Build.VERSION.SDK_INT >= 25) {
val manager = context.getSystemService<ShortcutManager>()
manager?.dynamicShortcuts = getShortCuts(context)
manager?.dynamicShortcuts =
getShortCuts(context)
}
}
......@@ -77,19 +78,6 @@ object Shortcuts {
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
)
.setIcon(getIcon(R.drawable.sc_extension))
.setRank(3)
.build()
)
shortCuts.add(
ShortcutInfo.Builder(context, "downloads")
.setShortLabel(context.getString(R.string.downloads))
.setIntent(
Intent(intent)
.putExtra(Const.Key.OPEN_SECTION, "downloads")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
)
.setIcon(getIcon(R.drawable.sc_cloud_download))
.setRank(2)
.build()
)
......
@file:JvmMultifileClass
package com.topjohnwu.magisk.data.database
import androidx.room.Dao
import androidx.room.Query
import com.topjohnwu.magisk.core.model.module.Repo
interface RepoBase {
fun getRepos(offset: Int, limit: Int = LIMIT): List<Repo>
fun searchRepos(query: String, offset: Int, limit: Int = LIMIT): List<Repo>
@Query("SELECT * FROM repos WHERE id = :id AND versionCode > :versionCode LIMIT 1")
fun getUpdatableRepoById(id: String, versionCode: Int): Repo?
@Query("SELECT * FROM repos WHERE id = :id LIMIT 1")
fun getRepoById(id: String): Repo?
companion object {
const val LIMIT = 10
}
}
@Dao
interface RepoByUpdatedDao : RepoBase {
@Query("SELECT * FROM repos ORDER BY last_update DESC LIMIT :limit OFFSET :offset")
override fun getRepos(offset: Int, limit: Int): List<Repo>
@Query(
"""SELECT *
FROM repos
WHERE
(author LIKE '%' || :query || '%') ||
(name LIKE '%' || :query || '%') ||
(description LIKE '%' || :query || '%')
ORDER BY last_update DESC
LIMIT :limit
OFFSET :offset"""
)
override fun searchRepos(query: String, offset: Int, limit: Int): List<Repo>
}
@Dao
interface RepoByNameDao : RepoBase {
@Query("SELECT * FROM repos ORDER BY name COLLATE NOCASE LIMIT :limit OFFSET :offset")
override fun getRepos(offset: Int, limit: Int): List<Repo>
@Query(
"""SELECT *
FROM repos
WHERE
(author LIKE '%' || :query || '%') ||
(name LIKE '%' || :query || '%') ||
(description LIKE '%' || :query || '%')
ORDER BY name COLLATE NOCASE
LIMIT :limit
OFFSET :offset"""
)
override fun searchRepos(query: String, offset: Int, limit: Int): List<Repo>
}
package com.topjohnwu.magisk.data.database
import androidx.room.*
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.model.entity.module.Repo
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.module.Repo
@Database(version = 6, entities = [Repo::class, RepoEtag::class])
abstract class RepoDatabase : RoomDatabase() {
abstract fun repoDao() : RepoDao
abstract fun repoByUpdatedDao(): RepoByUpdatedDao
abstract fun repoByNameDao(): RepoByNameDao
}
@Dao
......
package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.model.entity.UpdateInfo
import com.topjohnwu.magisk.tasks.GithubRepoInfo
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.tasks.GithubRepoInfo
import com.topjohnwu.magisk.core.model.UpdateInfo
import io.reactivex.Flowable
import io.reactivex.Single
import okhttp3.ResponseBody
......@@ -78,4 +78,4 @@ interface GithubApiServices {
@Query("sort") sort: String = "pushed",
@Query("per_page") count: Int = 100): Flowable<Result<List<GithubRepoInfo>>>
}
\ No newline at end of file
}
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.data.database.SettingsDao
import com.topjohnwu.magisk.data.database.StringDao
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
import com.topjohnwu.magisk.core.magiskdb.StringDao
import io.reactivex.schedulers.Schedulers
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
......
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.data.database.SuLogDao
import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import com.topjohnwu.superuser.Shell
import io.reactivex.Completable
import io.reactivex.Single
import java.util.concurrent.TimeUnit
class LogRepository(
private val logDao: SuLogDao
) {
fun fetchLogs() = logDao.fetchAll().map { it.wrap() }
fun fetchLogs() = logDao.fetchAll()
fun fetchMagiskLogs() = Single.fromCallable {
Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").exec().out
......@@ -28,11 +26,4 @@ class LogRepository(
fun insert(log: MagiskLog) = logDao.insert(log)
private fun List<MagiskLog>.wrap(): List<WrappedMagiskLog> {
val day = TimeUnit.DAYS.toMillis(1)
return groupBy { it.time / day }
.map { WrappedMagiskLog(it.key * day, it.value) }
}
}
package com.topjohnwu.magisk.data.repository
import android.content.pm.PackageManager
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.extensions.getLabel
import com.topjohnwu.magisk.extensions.packageName
......@@ -24,7 +24,8 @@ class MagiskRepository(
Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate()
Config.Value.CANARY_DEBUG_CHANNEL -> apiRaw.fetchCanaryDebugUpdate()
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(Config.customChannelUrl)
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(
Config.customChannelUrl)
else -> throw IllegalArgumentException()
}.flatMap {
// If remote version is lower than current installed, try switching to beta
......
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.model.entity.module.Repo
class StringRepository(
private val api: GithubRawServices
......@@ -12,4 +12,4 @@ class StringRepository(
fun getMetadata(repo: Repo) = api.fetchModuleInfo(repo.id, "module.prop")
fun getReadme(repo: Repo) = api.fetchModuleInfo(repo.id, "README.md")
}
\ No newline at end of file
}
......@@ -6,6 +6,7 @@ import android.app.Application
import android.content.Context
import android.os.Build
import android.os.Bundle
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import com.topjohnwu.magisk.utils.RxBus
import org.koin.core.qualifier.named
......@@ -23,6 +24,7 @@ val applicationModule = module {
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
single { ActivityTracker() }
factory { get<ActivityTracker>().foreground ?: NullActivity }
single { LocalBroadcastManager.getInstance(get()) }
}
private fun createDEContext(context: Context): Context {
......
......@@ -2,8 +2,12 @@ package com.topjohnwu.magisk.di
import android.content.Context
import androidx.room.Room
import com.topjohnwu.magisk.data.database.*
import com.topjohnwu.magisk.tasks.RepoUpdater
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
import com.topjohnwu.magisk.core.magiskdb.StringDao
import com.topjohnwu.magisk.core.tasks.RepoUpdater
import com.topjohnwu.magisk.data.database.RepoDatabase
import com.topjohnwu.magisk.data.database.SuLogDatabase
import org.koin.dsl.module
......@@ -11,7 +15,10 @@ val databaseModule = module {
single { PolicyDao(get()) }
single { SettingsDao() }
single { StringDao() }
single { createRepoDatabase(get()).repoDao() }
single { createRepoDatabase(get()) }
single { get<RepoDatabase>().repoDao() }
single { get<RepoDatabase>().repoByNameDao() }
single { get<RepoDatabase>().repoByUpdatedDao() }
single { createSuLogDatabase(get(Protected)).suLogDao() }
single { RepoUpdater(get(), get()) }
}
......
......@@ -4,7 +4,7 @@ import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.data.network.GithubApiServices
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.net.Networking
......
package com.topjohnwu.magisk.di
import android.net.Uri
import com.topjohnwu.magisk.legacy.flash.FlashViewModel
import com.topjohnwu.magisk.legacy.surequest.SuRequestViewModel
import com.topjohnwu.magisk.ui.MainViewModel
import com.topjohnwu.magisk.ui.flash.FlashViewModel
import com.topjohnwu.magisk.ui.hide.HideViewModel
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.ui.install.InstallViewModel
import com.topjohnwu.magisk.ui.log.LogViewModel
import com.topjohnwu.magisk.ui.module.ModuleViewModel
import com.topjohnwu.magisk.ui.request.RequestViewModel
import com.topjohnwu.magisk.ui.safetynet.SafetynetViewModel
import com.topjohnwu.magisk.ui.settings.SettingsViewModel
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
import com.topjohnwu.magisk.ui.theme.ThemeViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val viewModelModules = module {
viewModel { MainViewModel() }
viewModel { HideViewModel(get()) }
viewModel { HomeViewModel(get()) }
viewModel { SuperuserViewModel(get(), get(), get(), get()) }
viewModel { HideViewModel(get(), get()) }
viewModel { LogViewModel(get()) }
viewModel { ModuleViewModel(get(), get(), get()) }
viewModel { LogViewModel(get(), get()) }
viewModel { RequestViewModel() }
viewModel { SafetynetViewModel(get()) }
viewModel { SettingsViewModel(get()) }
viewModel { SuperuserViewModel(get(), get(), get()) }
viewModel { ThemeViewModel() }
viewModel { InstallViewModel() }
viewModel { MainViewModel() }
// Legacy
viewModel { (action: String, file: Uri, additional: Uri) ->
FlashViewModel(action, file, additional, get())
}
......
......@@ -52,9 +52,9 @@ fun <T> Observable<T>.subscribeK(
fun <T> Single<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onSuccess: OnSuccessListener<T> = {}
onNext: OnSuccessListener<T> = {}
) = applySchedulers()
.subscribe(onSuccess, onError)
.subscribe(onNext, onError)
fun <T> Maybe<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
......@@ -198,5 +198,8 @@ fun <T> ObservableField<T>.toObservable(): Observable<T> {
fun <T : Any> T.toSingle() = Single.just(this)
fun <T1, T2, R> zip(t1: Single<T1>, t2: Single<T2>, zipper: (T1, T2) -> R) =
Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })
inline fun <T1, T2, R> zip(
t1: Single<T1>,
t2: Single<T2>,
crossinline zipper: (T1, T2) -> R
) = Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })
\ No newline at end of file
......@@ -28,14 +28,17 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import androidx.core.net.toUri
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.FileProvider
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.utils.DynamicClassLoader
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.currentLocale
import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import java.io.File
import java.io.FileNotFoundException
import java.text.SimpleDateFormat
import java.util.*
import java.lang.reflect.Array as JArray
val packageName: String get() = get<Context>().packageName
......@@ -283,7 +286,7 @@ fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.getDrawable(thi
* with respect to RTL layout direction
*/
fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
if (SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
) {
return end to start
......@@ -294,10 +297,10 @@ fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
fun Context.openUrl(url: String) = Utils.openLink(this, url.toUri())
@Suppress("FunctionName")
inline fun <reified T> T.DynamicClassLoader(apk: File)
= DynamicClassLoader(apk, T::class.java.classLoader)
inline fun <reified T> T.DynamicClassLoader(apk: File) =
DynamicClassLoader(apk, T::class.java.classLoader)
fun Context.unwrap() : Context {
fun Context.unwrap(): Context {
var context = this
while (true) {
if (context is ContextWrapper)
......@@ -309,3 +312,38 @@ fun Context.unwrap() : Context {
}
fun Uri.writeTo(file: File) = toFile().copyTo(file)
fun Context.hasPermissions(vararg permissions: String) = permissions.all {
ContextCompat.checkSelfPermission(this, it) == PERMISSION_GRANTED
}
private val securityLevelFormatter get() = SimpleDateFormat("yyyy-MM-dd",
currentLocale
)
/** Friendly reminder to seek newer roms or install oem updates. */
val isDeviceSecure: Boolean
get() {
val latestPermittedTime = Calendar.getInstance().apply {
time = securityLevelDate
add(Calendar.MONTH, 2)
}.time.time
return now in 0..latestPermittedTime
}
val securityLevelDate get() = securityLevelFormatter.parseOrNull(securityLevel) ?: Date(0)
val securityLevel
get() = if (SDK_INT >= Build.VERSION_CODES.M) {
Build.VERSION.SECURITY_PATCH
} else {
null
} ?: "1970-01-01" //never
val isSAR
get() = ShellUtils
.fastCmd("grep_prop ro.build.system_root_image")
.let { it.isNotEmpty() && it.toBoolean() }
val isAB
get() = ShellUtils
.fastCmd("grep_prop ro.build.ab_update")
.let { it.isNotEmpty() && it.toBoolean() }
package com.topjohnwu.magisk.extensions
import android.os.Build
import timber.log.Timber
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.text.SimpleDateFormat
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
......@@ -100,6 +102,9 @@ fun Locale.toLangTag(): String {
}
}
fun SimpleDateFormat.parseOrNull(date: String) =
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
// Reflection hacks
private val loadClass = ClassLoader::class.java.getMethod("loadClass", String::class.java)
......
package com.topjohnwu.magisk.extensions
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFileInputStream
import com.topjohnwu.superuser.io.SuFileOutputStream
......@@ -11,4 +11,6 @@ fun reboot(reason: String = if (Info.recovery) "recovery" else "") {
}
fun File.suOutputStream() = SuFileOutputStream(this)
fun File.suInputStream() = SuFileInputStream(this)
\ No newline at end of file
fun File.suInputStream() = SuFileInputStream(this)
val hasRoot get() = Shell.rootAccess()
......@@ -2,7 +2,15 @@ package com.topjohnwu.magisk.extensions
import android.content.res.Resources
val specialChars = arrayOf('!', '@', '#', '$', '%', '&', '?')
val specialChars = arrayOf('!', '@', '#', '$', '%', '&', '?')
fun String.replaceRandomWithSpecial(passes: Int): String {
var string = this
repeat(passes) {
string = string.replaceRandomWithSpecial()
}
return string
}
fun String.replaceRandomWithSpecial(): String {
var random: Char
......
package com.topjohnwu.magisk.extensions
import com.topjohnwu.magisk.utils.currentLocale
import com.topjohnwu.magisk.core.utils.currentLocale
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
......@@ -14,7 +14,22 @@ fun String.toTime(format: DateFormat) = try {
-1L
}
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", currentLocale) }
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", currentLocale) }
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, currentLocale) }
val timeFormatTime by lazy { SimpleDateFormat("h:mm a", currentLocale) }
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss",
currentLocale
) }
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'",
currentLocale
) }
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM,
currentLocale
) }
val timeFormatTime by lazy { SimpleDateFormat("h:mm a",
currentLocale
) }
val timeDateFormat by lazy {
DateFormat.getDateTimeInstance(
DateFormat.DEFAULT,
DateFormat.DEFAULT,
currentLocale
)
}
package com.topjohnwu.magisk.extensions
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
fun View.setOnViewReadyListener(callback: () -> Unit) = addOnGlobalLayoutListener(true, callback)
......@@ -11,4 +15,9 @@ fun View.addOnGlobalLayoutListener(oneShot: Boolean = false, callback: () -> Uni
if (oneShot) viewTreeObserver.removeOnGlobalLayoutListener(this)
callback()
}
})
\ No newline at end of file
})
fun ViewGroup.startAnimations() {
val transition = AutoTransition().setInterpolator(FastOutSlowInInterpolator()).setDuration(400)
TransitionManager.beginDelayedTransition(this, transition)
}
\ No newline at end of file
package com.topjohnwu.magisk.ui.flash
package com.topjohnwu.magisk.legacy.flash
import android.content.Context
import android.content.Intent
......@@ -6,22 +6,22 @@ import android.content.pm.ActivityInfo
import android.net.Uri
import android.os.Bundle
import androidx.core.net.toUri
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.databinding.ActivityFlashBinding
import com.topjohnwu.magisk.extensions.snackbar
import com.topjohnwu.magisk.intent
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.ui.base.BaseUIActivity
import com.topjohnwu.magisk.core.view.Notifications
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import java.io.File
open class FlashActivity : BaseActivity<FlashViewModel, ActivityFlashBinding>() {
open class FlashActivity : BaseUIActivity<FlashViewModel, ActivityFlashBinding>() {
override val layoutRes: Int = R.layout.activity_flash
override val themeRes: Int = R.style.MagiskTheme_Flashing
......
package com.topjohnwu.magisk.legacy.flash
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.content.res.Resources
import android.net.Uri
import android.os.Handler
import androidx.core.os.postDelayed
import androidx.databinding.ObservableArrayList
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.model.flash.FlashResultListener
import com.topjohnwu.magisk.model.flash.Flashing
import com.topjohnwu.magisk.model.flash.Patching
import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.superuser.Shell
import me.tatarka.bindingcollectionadapter2.ItemBinding
import java.io.File
import java.util.*
class FlashViewModel(
action: String,
installer: Uri,
uri: Uri,
private val resources: Resources
) : BaseViewModel(), FlashResultListener {
val canShowReboot = Shell.rootAccess()
val showRestartTitle = KObservableField(false)
val behaviorText = KObservableField(resources.getString(R.string.flashing))
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@FlashViewModel)
}
private val outItems = ObservableArrayList<String>()
private val logItems = Collections.synchronizedList(mutableListOf<String>())
init {
outItems.sendUpdatesTo(items) { it.map { ConsoleRvItem(it) } }
outItems.copyNewInputInto(logItems)
state = State.LOADING
when (action) {
Const.Value.FLASH_ZIP -> Flashing
.Install(installer, outItems, logItems, this)
.exec()
Const.Value.UNINSTALL -> Flashing
.Uninstall(installer, outItems, logItems, this)
.exec()
Const.Value.FLASH_MAGISK -> Patching
.Direct(installer, outItems, logItems, this)
.exec()
Const.Value.FLASH_INACTIVE_SLOT -> Patching
.SecondSlot(installer, outItems, logItems, this)
.exec()
Const.Value.PATCH_FILE -> Patching
.File(installer, uri, outItems, logItems, this)
.exec()
}
}
override fun onResult(isSuccess: Boolean) {
state = if (isSuccess) State.LOADED else State.LOADING_FAILED
behaviorText.value = when {
isSuccess -> resources.getString(R.string.done)
else -> resources.getString(R.string.failure)
}
if (isSuccess) {
Handler().postDelayed(500) {
showRestartTitle.value = true
}
}
}
fun savePressed() = withPermissions(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE)
.map { now }
.map { it.toTime(timeFormatStandard) }
.map { Const.MAGISK_INSTALL_LOG_FILENAME.format(it) }
.map { File(Config.downloadDirectory, it) }
.map { file ->
file.bufferedWriter().use { writer ->
logItems.forEach {
writer.write(it)
writer.newLine()
}
}
file.path
}
.subscribeK { SnackbarEvent(it).publish() }
.add()
fun restartPressed() = reboot()
fun backPressed() = back()
}
package com.topjohnwu.magisk.ui.surequest
package com.topjohnwu.magisk.legacy.surequest
import android.content.Intent
import android.content.pm.ActivityInfo
......@@ -6,16 +6,16 @@ import android.os.Build
import android.os.Bundle
import android.view.Window
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.core.su.SuCallbackHandler
import com.topjohnwu.magisk.core.su.SuCallbackHandler.REQUEST
import com.topjohnwu.magisk.databinding.ActivityRequestBinding
import com.topjohnwu.magisk.model.events.DieEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.utils.SuHandler
import com.topjohnwu.magisk.utils.SuHandler.REQUEST
import com.topjohnwu.magisk.ui.base.BaseUIActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestBinding>() {
open class SuRequestActivity : BaseUIActivity<SuRequestViewModel, ActivityRequestBinding>() {
override val layoutRes: Int = R.layout.activity_request
override val themeRes: Int = R.style.MagiskTheme_SU
......@@ -36,7 +36,11 @@ open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestB
}
fun runHandler(action: String?) {
SuHandler(this, action, intent.extras)
SuCallbackHandler(
this,
action,
intent.extras
)
finish()
}
......
package com.topjohnwu.magisk.legacy.surequest
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.CountDownTimer
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.ALLOW
import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.DENY
import com.topjohnwu.magisk.core.su.SuRequestHandler
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem
import com.topjohnwu.magisk.model.events.DieEvent
import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.core.utils.BiometricHelper
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter
import me.tatarka.bindingcollectionadapter2.ItemBinding
import java.util.concurrent.TimeUnit.SECONDS
class SuRequestViewModel(
private val pm: PackageManager,
private val policyDB: PolicyDao,
private val timeoutPrefs: SharedPreferences,
private val res: Resources
) : BaseViewModel() {
val icon = KObservableField<Drawable?>(null)
val title = KObservableField("")
val packageName = KObservableField("")
val denyText = KObservableField(res.getString(R.string.deny))
val warningText = KObservableField<CharSequence>(res.getString(R.string.su_warning))
val selectedItemPosition = KObservableField(0)
private val items = DiffObservableList(ComparableRvItem.callback)
private val itemBinding = ItemBinding.of<ComparableRvItem<*>> { binding, _, item ->
item.bind(binding)
}
val adapter = BindingListViewAdapter<ComparableRvItem<*>>(1).apply {
itemBinding = this@SuRequestViewModel.itemBinding
setItems(items)
}
private val handler = Handler()
fun grantPressed() {
handler.cancelTimer()
if (BiometricHelper.isEnabled) {
withView {
BiometricHelper.authenticate(this) {
handler.respond(ALLOW)
}
}
} else {
handler.respond(ALLOW)
}
}
fun denyPressed() {
handler.respond(DENY)
}
fun spinnerTouched(): Boolean {
handler.cancelTimer()
return false
}
fun handleRequest(intent: Intent): Boolean {
return handler.start(intent)
}
private inner class Handler : SuRequestHandler(pm, policyDB) {
fun respond(action: Int) {
val pos = selectedItemPosition.value
timeoutPrefs.edit().putInt(policy.packageName, pos).apply()
respond(action, Config.Value.TIMEOUT_LIST[pos])
}
fun cancelTimer() {
timer.cancel()
denyText.value = res.getString(R.string.deny)
}
override fun onStart() {
res.getStringArray(R.array.allow_timeout)
.map { SpinnerRvItem(it) }
.let { items.update(it) }
icon.value = policy.applicationInfo.loadIcon(pm)
title.value = policy.appName
packageName.value = policy.packageName
selectedItemPosition.value = timeoutPrefs.getInt(policy.packageName, 0)
// Override timer
val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
timer = object : CountDownTimer(millis, 1000) {
override fun onTick(remains: Long) {
denyText.value = "${res.getString(R.string.deny)} (${remains / 1000})"
}
override fun onFinish() {
denyText.value = res.getString(R.string.deny)
respond(DENY)
}
}
}
override fun onRespond() {
// Kill activity after response
DieEvent().publish()
}
}
}
......@@ -14,3 +14,14 @@ class HideAppInfo(
val processes = info.packageInfo?.processes?.distinct() ?: listOf(info.packageName)
}
data class StatefulProcess(
val name: String,
val packageName: String,
val isHidden: Boolean
)
class ProcessHideApp(
val info: HideAppInfo,
val processes: List<StatefulProcess>
)
\ No newline at end of file
......@@ -3,10 +3,11 @@ package com.topjohnwu.magisk.model.entity
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.core.model.MagiskPolicy
import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.ALLOW
import com.topjohnwu.magisk.extensions.now
import com.topjohnwu.magisk.extensions.timeFormatTime
import com.topjohnwu.magisk.extensions.toTime
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.ALLOW
@Entity(tableName = "logs")
data class MagiskLog(
......@@ -23,11 +24,6 @@ data class MagiskLog(
@Ignore val timeString = time.toTime(timeFormatTime)
}
data class WrappedMagiskLog(
val time: Long,
val items: List<MagiskLog>
)
fun MagiskPolicy.toLog(
toUid: Int,
fromPid: Int,
......
......@@ -2,13 +2,13 @@ package com.topjohnwu.magisk.model.entity.internal
import android.content.Context
import android.os.Parcelable
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.extensions.cachedFile
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.model.entity.MagiskJson
import com.topjohnwu.magisk.model.entity.ManagerJson
import com.topjohnwu.magisk.model.entity.module.Repo
import com.topjohnwu.magisk.core.model.MagiskJson
import com.topjohnwu.magisk.core.model.ManagerJson
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
import java.io.File
......@@ -59,7 +59,7 @@ sealed class DownloadSubject : Parcelable {
val magisk: MagiskJson = Info.remote.magisk
@Parcelize
protected data class Flash(
data class Flash(
override val configuration: Configuration
) : Magisk() {
override val url: String get() = magisk.link
......@@ -72,7 +72,7 @@ sealed class DownloadSubject : Parcelable {
}
@Parcelize
protected class Uninstall : Magisk() {
class Uninstall : Magisk() {
override val configuration: Configuration get() = Configuration.Uninstall
override val url: String get() = Info.remote.uninstaller.link
......@@ -83,7 +83,7 @@ sealed class DownloadSubject : Parcelable {
}
@Parcelize
protected class Download : Magisk() {
class Download : Magisk() {
override val configuration: Configuration get() = Configuration.Download
override val url: String get() = magisk.link
......@@ -103,4 +103,4 @@ sealed class DownloadSubject : Parcelable {
}
}
\ No newline at end of file
}
......@@ -6,7 +6,7 @@ import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() {
open class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() {
override val layoutRes: Int = R.layout.item_console
override fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {
......@@ -23,4 +23,8 @@ class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() {
override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other)
override fun itemSameAs(other: ConsoleRvItem) = item == other.item
}
class ConsoleItem(item: String) : ConsoleRvItem(item) {
override val layoutRes = R.layout.item_console_md2
}
\ No newline at end of file
package com.topjohnwu.magisk.model.entity.recycler
import android.view.View
import android.view.ViewGroup
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.startAnimations
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.model.entity.ProcessHideApp
import com.topjohnwu.magisk.model.entity.StatefulProcess
import com.topjohnwu.magisk.model.observer.Observer
import com.topjohnwu.magisk.ui.hide.HideViewModel
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
ComparableRvItem<HideRvItem>() {
override val layoutRes: Int = R.layout.item_hide_app
val packageName = item.info.packageName.orEmpty()
val items = DiffObservableList(callback).also {
val items = item.processes.map {
val isHidden = targets.any { target ->
packageName == target.packageName && it == target.process
}
HideProcessRvItem(packageName, it, isHidden)
}
it.update(items)
}
val isHiddenState = KObservableField(currentState)
val isExpanded = KObservableField(false)
import kotlin.math.roundToInt
class HideItem(val item: ProcessHideApp) : ComparableRvItem<HideItem>() {
override val layoutRes = R.layout.item_hide_md2
private val itemsProcess get() = items.filterIsInstance<HideProcessRvItem>()
val packageName = item.info.info.packageName.orEmpty()
val items = item.processes.map { HideProcessItem(it) }
val isExpanded = KObservableField(false)
val itemsChecked = KObservableField(0)
val itemsCheckedPercent = Observer(itemsChecked) {
(itemsChecked.value.toFloat() / items.size * 100).roundToInt()
}
private val currentState
get() = when (itemsProcess.count { it.isHidden.value }) {
items.size -> IndeterminateState.CHECKED
in 1 until items.size -> IndeterminateState.INDETERMINATE
else -> IndeterminateState.UNCHECKED
}
/** [toggle] depends on this functionality */
private val isHidden get() = itemsChecked.value == items.size
init {
itemsProcess.forEach {
it.isHidden.addOnPropertyChangedCallback { isHiddenState.value = currentState }
}
items.forEach { it.isHidden.addOnPropertyChangedCallback { recalculateChecked() } }
recalculateChecked()
}
fun toggle() {
val desiredState = when (isHiddenState.value) {
IndeterminateState.INDETERMINATE,
IndeterminateState.UNCHECKED -> true
IndeterminateState.CHECKED -> false
}
itemsProcess.forEach { it.isHidden.value = desiredState }
isHiddenState.value = currentState
fun collapse(v: View) {
(v.parent.parent as? ViewGroup)?.startAnimations()
isExpanded.value = false
}
fun toggleExpansion() {
if (items.size <= 1) return
fun toggle(v: View) {
(v.parent as? ViewGroup)?.startAnimations()
isExpanded.toggle()
}
override fun contentSameAs(other: HideRvItem): Boolean = items.all { other.items.contains(it) }
override fun itemSameAs(other: HideRvItem): Boolean = item.info == other.item.info
fun toggle(viewModel: HideViewModel): Boolean {
// contract implies that isHidden == all checked
if (!isHidden) {
items.filterNot { it.isHidden.value }
} else {
items
}.forEach { it.toggle(viewModel) }
return true
}
}
private fun recalculateChecked() {
itemsChecked.value = items.count { it.isHidden.value }
}
class HideProcessRvItem(
val packageName: String,
val process: String,
isHidden: Boolean
) : ComparableRvItem<HideProcessRvItem>() {
override fun contentSameAs(other: HideItem): Boolean = item == other.item
override fun itemSameAs(other: HideItem): Boolean = item.info == other.item.info
override val layoutRes: Int = R.layout.item_hide_process
}
val isHidden = KObservableField(isHidden)
class HideProcessItem(val item: StatefulProcess) : ComparableRvItem<HideProcessItem>() {
private val rxBus: RxBus by inject()
override val layoutRes = R.layout.item_hide_process_md2
init {
this.isHidden.addOnPropertyChangedCallback {
rxBus.post(HideProcessEvent(this@HideProcessRvItem))
}
val isHidden = KObservableField(item.isHidden)
fun toggle(viewModel: HideViewModel) {
isHidden.toggle()
viewModel.toggleItem(this)
}
fun toggle() = isHidden.toggle()
override fun contentSameAs(other: HideProcessItem) = item == other.item
override fun itemSameAs(other: HideProcessItem) = item.name == other.item.name
override fun contentSameAs(other: HideProcessRvItem): Boolean = itemSameAs(other)
override fun itemSameAs(other: HideProcessRvItem): Boolean =
packageName == other.packageName && process == other.process
}
\ No newline at end of file
}
package com.topjohnwu.magisk.model.entity.recycler
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.databinding.ComparableRvItem
sealed class HomeItem : ComparableRvItem<HomeItem>() {
abstract val icon: Int
abstract val title: Int
abstract val link: String
override val layoutRes = R.layout.item_developer_link
override fun contentSameAs(other: HomeItem) = itemSameAs(other)
override fun itemSameAs(other: HomeItem) = this == other
override fun equals(other: Any?): Boolean {
if (other !is HomeItem) return false
return icon == other.icon && title == other.title && link == other.link
}
override fun hashCode() =
icon.hashCode() + title.hashCode() + link.hashCode() + layoutRes.hashCode()
// region Children
sealed class PayPal : HomeItem() {
override val icon = R.drawable.ic_paypal
override val title = R.string.home_item_paypal
override val link = "https://paypal.me/%s"
// region Children
object App : PayPal() {
override val link = super.link.format("diareuse")
}
object Mainline : PayPal() {
override val link = super.link.format("topjohnwu")
}
// endregion
}
object Patreon : HomeItem() {
override val icon = R.drawable.ic_patreon
override val title = R.string.home_item_patreon
override val link = Const.Url.PATREON_URL
}
sealed class Twitter : HomeItem() {
override val icon = R.drawable.ic_twitter
override val title = R.string.home_item_twitter
override val link = "https://twitter.com/%s"
// region Children
object App : Twitter() {
override val link = super.link.format("diareuse")
}
object Mainline : Twitter() {
override val link = super.link.format("topjohnwu")
}
// endregion
}
object Github : HomeItem() {
override val icon = R.drawable.ic_github
override val title = R.string.home_item_source
override val link = Const.Url.SOURCE_CODE_URL
}
object Xda : HomeItem() {
override val icon = R.drawable.ic_xda
override val title = R.string.home_item_xda
override val link = Const.Url.XDA_THREAD
}
// endregion
}
sealed class DeveloperItem : ComparableRvItem<DeveloperItem>() {
abstract val items: List<HomeItem>
abstract val name: Int
override val layoutRes = R.layout.item_developer
override fun contentSameAs(other: DeveloperItem) = itemSameAs(other)
override fun itemSameAs(other: DeveloperItem) = this == other
override fun equals(other: Any?): Boolean {
if (other !is DeveloperItem) return false
return name == other.name && items == other.items
}
override fun hashCode() = name.hashCode() + items.hashCode() + layoutRes.hashCode()
//region Children
object Mainline : DeveloperItem() {
override val items =
listOf(HomeItem.PayPal.Mainline, HomeItem.Patreon, HomeItem.Twitter.Mainline)
override val name = R.string.home_links_mainline
}
object App : DeveloperItem() {
override val items =
listOf(HomeItem.PayPal.App, HomeItem.Twitter.App)
override val name = R.string.home_links_app
}
object Project : DeveloperItem() {
override val items =
listOf(HomeItem.Github, HomeItem.Xda)
override val name = R.string.home_links_project
}
//endregion
}
package com.topjohnwu.magisk.model.entity.recycler
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.timeFormatMedium
import com.topjohnwu.magisk.extensions.timeDateFormat
import com.topjohnwu.magisk.extensions.toTime
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
class LogRvItem : ComparableRvItem<LogRvItem>() {
override val layoutRes: Int = R.layout.item_page_log
val items = DiffObservableList(callback)
fun update(list: List<LogItemRvItem>) {
list.firstOrNull()?.isExpanded?.value = true
items.update(list)
}
//two of these will never be present, safe to assume it's unique
override fun contentSameAs(other: LogRvItem): Boolean = false
override fun itemSameAs(other: LogRvItem): Boolean = false
}
class LogItemRvItem(
item: WrappedMagiskLog
) : ComparableRvItem<LogItemRvItem>() {
override val layoutRes: Int = R.layout.item_superuser_log
val date = item.time.toTime(timeFormatMedium)
val items: List<ComparableRvItem<*>> = item.items.map { LogItemEntryRvItem(it) }
val isExpanded = KObservableField(false)
fun toggle() = isExpanded.toggle()
override fun contentSameAs(other: LogItemRvItem): Boolean {
if (items.size != other.items.size) return false
return items.all { it in other.items }
}
override fun itemSameAs(other: LogItemRvItem): Boolean = date == other.date
}
class LogItemEntryRvItem(val item: MagiskLog) : ComparableRvItem<LogItemEntryRvItem>() {
override val layoutRes: Int = R.layout.item_superuser_log_entry
val isExpanded = KObservableField(false)
fun toggle() = isExpanded.toggle()
override fun contentSameAs(other: LogItemEntryRvItem) = item == other.item
override fun itemSameAs(other: LogItemEntryRvItem) = item.appName == other.item.appName
}
class MagiskLogRvItem : ComparableRvItem<MagiskLogRvItem>() {
override val layoutRes: Int = R.layout.item_page_magisk_log
val items = DiffObservableList(callback)
fun update(list: List<ConsoleRvItem>) {
items.update(list)
}
//two of these will never be present, safe to assume it's unique
override fun contentSameAs(other: MagiskLogRvItem): Boolean = false
override fun itemSameAs(other: MagiskLogRvItem): Boolean = false
class LogItem(val item: MagiskLog) : ObservableItem<LogItem>() {
override val layoutRes = R.layout.item_log_access_md2
val date = item.time.toTime(timeDateFormat)
var isTop = false
@Bindable get
set(value) {
field = value
notifyChange(BR.top)
}
var isBottom = false
@Bindable get
set(value) {
field = value
notifyChange(BR.bottom)
}
override fun itemSameAs(other: LogItem) = item.appName == other.item.appName
override fun contentSameAs(other: LogItem) = item.fromUid == other.item.fromUid &&
item.toUid == other.item.toUid &&
item.fromPid == other.item.fromPid &&
item.packageName == other.item.packageName &&
item.command == other.item.command &&
item.action == other.item.action &&
item.time == other.item.time &&
isTop == other.isTop &&
isBottom == other.isBottom
}
package com.topjohnwu.magisk.model.entity.recycler
import android.content.res.Resources
import androidx.annotation.StringRes
import androidx.databinding.Bindable
import androidx.databinding.Observable
import androidx.databinding.PropertyChangeRegistry
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.model.module.Module
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.module.Module
import com.topjohnwu.magisk.model.entity.module.Repo
import com.topjohnwu.magisk.ui.module.ModuleViewModel
import com.topjohnwu.magisk.utils.KObservableField
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
override val layoutRes: Int = R.layout.item_module
val lastActionNotice = KObservableField("")
val isChecked = KObservableField(item.enable)
val isDeletable = KObservableField(item.remove)
init {
isChecked.addOnPropertyChangedCallback {
when (it) {
true -> {
item.enable = true
notice(R.string.disable_file_removed)
}
false -> {
item.enable = false
notice(R.string.disable_file_created)
}
}
object SafeModeNotice : ComparableRvItem<SafeModeNotice>() {
override val layoutRes = R.layout.item_safe_mode_notice
override fun onBindingBound(binding: ViewDataBinding) {
super.onBindingBound(binding)
val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams
params?.isFullSpan = true
}
override fun contentSameAs(other: SafeModeNotice) = this == other
override fun itemSameAs(other: SafeModeNotice) = this === other
}
object InstallModule : ComparableRvItem<InstallModule>() {
override val layoutRes = R.layout.item_module_download
override fun onBindingBound(binding: ViewDataBinding) {
super.onBindingBound(binding)
val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams
params?.isFullSpan = true
}
override fun contentSameAs(other: InstallModule) = this == other
override fun itemSameAs(other: InstallModule) = this === other
}
class SectionTitle(
val title: Int,
_button: Int = 0,
_icon: Int = 0
) : ObservableItem<SectionTitle>() {
override val layoutRes = R.layout.item_section_md2
var button = _button
@Bindable get
set(value) {
field = value
notifyChange(BR.button)
}
var icon = _icon
@Bindable get
set(value) {
field = value
notifyChange(BR.icon)
}
var hasButton = button != 0 || icon != 0
@Bindable get
set(value) {
field = value
notifyChange(BR.hasButton)
}
isDeletable.addOnPropertyChangedCallback {
when (it) {
true -> {
item.remove = true
notice(R.string.remove_file_created)
}
false -> {
item.remove = false
notice(R.string.remove_file_deleted)
}
}
override fun onBindingBound(binding: ViewDataBinding) {
super.onBindingBound(binding)
val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams
params?.isFullSpan = true
}
override fun itemSameAs(other: SectionTitle): Boolean = this === other
override fun contentSameAs(other: SectionTitle): Boolean = this === other
}
sealed class RepoItem(val item: Repo) : ObservableItem<RepoItem>() {
override val layoutRes: Int = R.layout.item_repo_md2
val progress = KObservableField(0)
var isUpdate = false
@Bindable get
protected set(value) {
field = value
notifyChange(BR.update)
}
when {
item.updated -> notice(R.string.update_file_created)
item.remove -> notice(R.string.remove_file_created)
override fun contentSameAs(other: RepoItem): Boolean = item == other.item
override fun itemSameAs(other: RepoItem): Boolean = item.id == other.item.id
class Update(item: Repo) : RepoItem(item) {
init {
isUpdate = true
}
}
fun toggle() = isChecked.toggle()
fun toggleDelete() = isDeletable.toggle()
class Remote(item: Repo) : RepoItem(item)
}
class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
override val layoutRes = R.layout.item_module_md2
@get:Bindable
var repo: Repo? = null
set(value) {
field = value
notifyChange(BR.repo)
}
@get:Bindable
var isEnabled = item.enable
set(value) {
field = value
item.enable = value
notifyChange(BR.enabled)
}
@get:Bindable
var isRemoved = item.remove
set(value) {
field = value
item.remove = value
notifyChange(BR.removed)
}
private fun notice(@StringRes info: Int) {
lastActionNotice.value = get<Resources>().getString(info)
val isUpdated get() = item.updated
val isModified get() = isRemoved || item.updated
fun toggle() {
isEnabled = !isEnabled
}
fun delete(viewModel: ModuleViewModel) {
isRemoved = !isRemoved
viewModel.updateActiveState()
}
override fun contentSameAs(other: ModuleRvItem): Boolean = item.version == other.item.version
override fun contentSameAs(other: ModuleItem): Boolean = item.version == other.item.version
&& item.versionCode == other.item.versionCode
&& item.description == other.item.description
&& item.name == other.item.name
override fun itemSameAs(other: ModuleRvItem): Boolean = item.id == other.item.id
override fun itemSameAs(other: ModuleItem): Boolean = item.id == other.item.id
}
class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
abstract class ObservableItem<T> : ComparableRvItem<T>(), Observable {
override val layoutRes: Int = R.layout.item_repo
private val list = PropertyChangeRegistry()
override fun contentSameAs(other: RepoRvItem): Boolean = item == other.item
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
list.remove(callback ?: return)
}
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
}
\ No newline at end of file
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
list.add(callback ?: return)
}
fun notifyChange(id: Int) = list.notifyChange(this, id)
}
......@@ -2,51 +2,52 @@ package com.topjohnwu.magisk.model.entity.recycler
import android.graphics.drawable.Drawable
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.model.MagiskPolicy
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
override val layoutRes: Int = R.layout.item_policy
class PolicyItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyItem>() {
override val layoutRes = R.layout.item_policy_md2
val isExpanded = KObservableField(false)
val isEnabled = KObservableField(item.policy == MagiskPolicy.ALLOW)
val shouldNotify = KObservableField(item.notification)
val shouldLog = KObservableField(item.logging)
fun toggle() = isExpanded.toggle()
private val rxBus: RxBus by inject()
private val currentStateItem
private val updatedPolicy
get() = item.copy(
policy = if (isEnabled.value) MagiskPolicy.ALLOW else MagiskPolicy.DENY,
notification = shouldNotify.value,
logging = shouldLog.value
)
init {
isEnabled.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
rxBus.post(PolicyEnableEvent(this@PolicyRvItem, it))
}
shouldNotify.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
rxBus.post(PolicyUpdateEvent.Notification(currentStateItem))
}
shouldLog.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
rxBus.post(PolicyUpdateEvent.Log(currentStateItem))
fun toggle(viewModel: SuperuserViewModel) {
if (isExpanded.value) {
toggle()
return
}
isEnabled.toggle()
viewModel.togglePolicy(this, isEnabled.value)
}
fun toggle() {
isExpanded.toggle()
}
fun toggleNotify(viewModel: SuperuserViewModel) {
shouldNotify.toggle()
viewModel.updatePolicy(PolicyUpdateEvent.Notification(updatedPolicy))
}
override fun contentSameAs(other: PolicyRvItem): Boolean = itemSameAs(other)
override fun itemSameAs(other: PolicyRvItem): Boolean = item.uid == other.item.uid
}
\ No newline at end of file
fun toggleLog(viewModel: SuperuserViewModel) {
shouldLog.toggle()
viewModel.updatePolicy(PolicyUpdateEvent.Log(updatedPolicy))
}
override fun contentSameAs(other: PolicyItem) = itemSameAs(other)
override fun itemSameAs(other: PolicyItem) = item.uid == other.item.uid
}
package com.topjohnwu.magisk.model.entity.recycler
import android.content.Context
import android.content.res.Resources
import android.view.MotionEvent
import android.view.View
import androidx.annotation.CallSuper
import androidx.databinding.Bindable
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.utils.TransitiveText
import com.topjohnwu.magisk.view.MagiskDialog
import org.koin.core.KoinComponent
import org.koin.core.get
import kotlin.properties.ObservableProperty
import kotlin.reflect.KProperty
sealed class SettingsItem : ObservableItem<SettingsItem>() {
open val icon: Int get() = 0
open val title: TransitiveText get() = TransitiveText.EMPTY
@get:Bindable
open val description: TransitiveText get() = TransitiveText.EMPTY
@get:Bindable
var isEnabled by bindable(true, BR.enabled)
protected open val isFullSpan get() = false
@CallSuper
open fun onPressed(view: View, callback: Callback) {
callback.onItemChanged(view, this)
// notify only after the callback invocation; callback can invalidate the backing data,
// which wouldn't be recognized with reverse approach
notifyChange(BR.description)
}
open fun refresh() {}
override fun onBindingBound(binding: ViewDataBinding) {
super.onBindingBound(binding)
if (isFullSpan) {
val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams
params?.isFullSpan = true
}
}
override fun itemSameAs(other: SettingsItem) = this === other
override fun contentSameAs(other: SettingsItem) = itemSameAs(other)
protected inline fun <T> bindable(
initialValue: T,
fieldId: Int,
crossinline setter: (T) -> Unit = {}
) = object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) {
setter(newValue)
notifyChange(fieldId)
}
}
// ---
interface Callback {
fun onItemPressed(view: View, item: SettingsItem)
fun onItemChanged(view: View, item: SettingsItem)
}
// ---
abstract class Value<T> : SettingsItem() {
@get:Bindable
abstract var value: T
protected inline fun bindableValue(
initialValue: T,
crossinline setter: (T) -> Unit
) = bindable(initialValue, BR.value, setter)
}
abstract class Toggle : Value<Boolean>() {
override val layoutRes = R.layout.item_settings_toggle
override fun onPressed(view: View, callback: Callback) {
callback.onItemPressed(view, this)
value = !value
super.onPressed(view, callback)
}
fun onTouched(view: View, callback: Callback, event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_UP) {
onPressed(view, callback)
}
return true
}
}
abstract class Input : Value<String>(), KoinComponent {
override val layoutRes = R.layout.item_settings_input
open val showStrip = true
protected val resources get() = get<Resources>()
protected abstract val intermediate: String?
override fun onPressed(view: View, callback: Callback) {
callback.onItemPressed(view, this)
MagiskDialog(view.context)
.applyTitle(title.getText(resources))
.applyView(getView(view.context))
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = android.R.string.ok
onClick {
intermediate?.let { result ->
preventDismiss = false
value = result
it.dismiss()
super.onPressed(view, callback)
return@onClick
}
preventDismiss = true
}
}
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = android.R.string.cancel
}
.reveal()
}
abstract fun getView(context: Context): View
}
abstract class Selector : Value<Int>(), KoinComponent {
override val layoutRes = R.layout.item_settings_selector
protected val resources get() = get<Resources>()
abstract val entries: Array<out CharSequence>
abstract val entryValues: Array<out CharSequence>
@get:Bindable
val selectedEntry
get() = entries.getOrNull(value)
override fun onPressed(view: View, callback: Callback) {
if (entries.isEmpty() || entryValues.isEmpty()) return
callback.onItemPressed(view, this)
MagiskDialog(view.context)
.applyTitle(title.getText(resources))
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = android.R.string.cancel
}
.applyAdapter(entries) {
value = it
notifyChange(BR.selectedEntry)
super.onPressed(view, callback)
}
.reveal()
}
}
abstract class Blank : SettingsItem() {
override val layoutRes = R.layout.item_settings_blank
override fun onPressed(view: View, callback: Callback) {
callback.onItemPressed(view, this)
super.onPressed(view, callback)
}
}
abstract class Section : SettingsItem() {
override val layoutRes = R.layout.item_settings_section
override val isFullSpan get() = true
}
}
package com.topjohnwu.magisk.model.entity.recycler
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
sealed class TappableHeadlineItem : ComparableRvItem<TappableHeadlineItem>() {
abstract val title: Int
abstract val icon: Int
override val layoutRes = R.layout.item_tappable_headline
override fun itemSameAs(other: TappableHeadlineItem) =
this === other
override fun contentSameAs(other: TappableHeadlineItem) =
title == other.title && icon == other.icon
// --- listener
interface Listener {
fun onItemPressed(item: TappableHeadlineItem)
}
// --- objects
object Hide : TappableHeadlineItem() {
override val title = R.string.magisk_hide_md2
override val icon = R.drawable.ic_hide_md2
}
object Safetynet : TappableHeadlineItem() {
override val title = R.string.safetyNet
override val icon = R.drawable.ic_safetynet_md2
}
object ThemeMode : TappableHeadlineItem() {
override val title = R.string.settings_dark_mode_title
override val icon = R.drawable.ic_day_night
}
}
\ No newline at end of file
package com.topjohnwu.magisk.model.entity.recycler
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
class TextItem(val text: Int) : ComparableRvItem<TextItem>() {
override val layoutRes = R.layout.item_text
override fun onBindingBound(binding: ViewDataBinding) {
super.onBindingBound(binding)
val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams
params?.isFullSpan = true
}
override fun contentSameAs(other: TextItem) = text == other.text
override fun itemSameAs(other: TextItem) = contentSameAs(other)
}
\ No newline at end of file
package com.topjohnwu.magisk.model.entity.recycler
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.ui.theme.Theme
class ThemeItem(val theme: Theme) : ComparableRvItem<ThemeItem>() {
override val layoutRes = R.layout.item_theme
override fun contentSameAs(other: ThemeItem) = itemSameAs(other)
override fun itemSameAs(other: ThemeItem) = theme == other.theme
}
package com.topjohnwu.magisk.model.events
import android.content.Context
import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.core.base.BaseActivity
interface ContextExecutor {
operator fun invoke(context: Context)
}
interface ActivityExecutor {
operator fun invoke(activity: BaseActivity)
}
interface FragmentExecutor {
operator fun invoke(fragment: Fragment)
}
package com.topjohnwu.magisk.model.events
import android.app.Activity
import android.content.Context
import android.content.Intent
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.legacy.flash.FlashActivity
class InstallExternalModuleEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseActivity) {
activity.withExternalRW {
onSuccess {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/zip"
activity.startActivityForResult(intent, Const.ID.FETCH_ZIP)
}
}
}
companion object {
fun onActivityResult(context: Context, requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) {
// Get the URI of the selected file
val intent = context.intent<FlashActivity>()
intent.setData(data.data).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP)
context.startActivity(intent)
}
}
}
}
package com.topjohnwu.magisk.model.events
import android.content.Context
import android.content.res.Resources
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import com.topjohnwu.magisk.R
data class OpenInappLinkEvent(
private val link: String
) : ViewEvent(), ContextExecutor {
// todo find app that can open the link and as a fallback open custom tabs! it shouldn't be the default
override fun invoke(context: Context) = CustomTabsIntent.Builder()
.setShowTitle(true)
.setToolbarColor(context.themedColor(R.attr.colorSurface))
.enableUrlBarHiding()
.build()
.launchUrl(context, link.toUri())
private fun Context.themedColor(@AttrRes attribute: Int) = theme
.resolveAttribute(attribute).data
private fun Resources.Theme.resolveAttribute(
@AttrRes attribute: Int,
resolveRefs: Boolean = true
) = TypedValue().also { resolveAttribute(attribute, it, resolveRefs) }
}
\ No newline at end of file
package com.topjohnwu.magisk.model.events
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
import com.topjohnwu.magisk.core.model.MagiskPolicy
import com.topjohnwu.magisk.utils.RxBus
class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event
class PolicyEnableEvent(val item: PolicyRvItem, val enable: Boolean) : RxBus.Event
sealed class PolicyUpdateEvent(val item: MagiskPolicy) : RxBus.Event {
class Notification(item: MagiskPolicy) : PolicyUpdateEvent(item)
class Log(item: MagiskPolicy) : PolicyUpdateEvent(item)
}
class ModuleUpdatedEvent(val item: ModuleRvItem) : RxBus.Event
data class SafetyNetResult(val responseCode: Int) : RxBus.Event
......@@ -3,13 +3,16 @@ package com.topjohnwu.magisk.model.events
import android.content.Context
import androidx.annotation.StringRes
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.extensions.snackbar
import com.topjohnwu.magisk.ui.base.BaseUIActivity
class SnackbarEvent private constructor(
@StringRes private val messageRes: Int,
private val messageString: String?,
val length: Int,
val f: Snackbar.() -> Unit
) : ViewEvent() {
) : ViewEvent(), ActivityExecutor {
constructor(
@StringRes messageRes: Int,
......@@ -24,4 +27,11 @@ class SnackbarEvent private constructor(
) : this(-1, message, length, f)
fun message(context: Context): String = messageString ?: context.getString(messageRes)
override fun invoke(activity: BaseActivity) {
if (activity is BaseUIActivity<*, *>) {
activity.snackbar(activity.snackbarView, message(activity), length, f)
}
}
}
package com.topjohnwu.magisk.model.events
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.model.entity.module.Repo
import android.app.Activity
import android.content.Context
import android.content.Intent
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.DynamicClassLoader
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.magisk.utils.RxBus
import com.topjohnwu.magisk.core.utils.SafetyNetHelper
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.superuser.Shell
import dalvik.system.DexFile
import io.reactivex.Completable
import io.reactivex.subjects.PublishSubject
import org.koin.core.KoinComponent
import org.koin.core.inject
import java.io.File
import java.lang.reflect.InvocationHandler
/**
* Class for passing events from ViewModels to Activities/Fragments
......@@ -15,33 +35,147 @@ abstract class ViewEvent {
var handled = false
}
data class OpenLinkEvent(val url: String) : ViewEvent()
class UpdateSafetyNetEvent : ViewEvent(), ContextExecutor, KoinComponent, SafetyNetHelper.Callback {
class ManagerInstallEvent : ViewEvent()
class MagiskInstallEvent : ViewEvent()
private val magiskRepo by inject<MagiskRepository>()
private val rxBus by inject<RxBus>()
class ManagerChangelogEvent : ViewEvent()
class MagiskChangelogEvent : ViewEvent()
private lateinit var EXT_APK: File
private lateinit var EXT_DEX: File
class UninstallEvent : ViewEvent()
class EnvFixEvent : ViewEvent()
override fun invoke(context: Context) {
val die = ::EXT_APK.isInitialized
class UpdateSafetyNetEvent : ViewEvent()
EXT_APK = File("${context.filesDir.parent}/snet", "snet.jar")
EXT_DEX = File(EXT_APK.parent, "snet.dex")
class ViewActionEvent(val action: BaseActivity<*, *>.() -> Unit) : ViewEvent()
Completable.fromAction {
val loader = DynamicClassLoader(EXT_APK)
val dex = DexFile.loadDex(EXT_APK.path, EXT_DEX.path, 0)
class OpenFilePickerEvent : ViewEvent()
// Scan through the dex and find our helper class
var helperClass: Class<*>? = null
for (className in dex.entries()) {
if (className.startsWith("x.")) {
val cls = loader.loadClass(className)
if (InvocationHandler::class.java.isAssignableFrom(cls)) {
helperClass = cls
break
}
}
}
helperClass ?: throw Exception()
class OpenChangelogEvent(val item: Repo) : ViewEvent()
class InstallModuleEvent(val item: Repo) : ViewEvent()
val helper = helperClass.getMethod(
"get",
Class::class.java, Context::class.java, Any::class.java
)
.invoke(null, SafetyNetHelper::class.java, context, this) as SafetyNetHelper
class PageChangedEvent : ViewEvent()
if (helper.version < Const.SNET_EXT_VER)
throw Exception()
helper.attest()
}.subscribeK(onError = {
if (die) {
rxBus.post(SafetyNetResult(-1))
} else {
Shell.sh("rm -rf " + EXT_APK.parent).exec()
EXT_APK.parentFile?.mkdir()
download(context, true)
}
})
}
@Suppress("SameParameterValue")
private fun download(context: Context, askUser: Boolean) {
fun downloadInternal() = magiskRepo.fetchSafetynet()
.map { it.byteStream().writeTo(EXT_APK) }
.subscribeK { invoke(context) }
if (!askUser) {
downloadInternal()
return
}
MagiskDialog(context)
.applyTitle(R.string.proprietary_title)
.applyMessage(R.string.proprietary_notice)
.cancellable(false)
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = R.string.yes
onClick { downloadInternal() }
}
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = android.R.string.no
onClick { rxBus.post(SafetyNetResult(-2)) }
}
.reveal()
}
override fun onResponse(responseCode: Int) {
rxBus.post(SafetyNetResult(responseCode))
}
}
class ViewActionEvent(val action: BaseActivity.() -> Unit) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseActivity) = activity.run(action)
}
class OpenChangelogEvent(val item: Repo) : ViewEvent(), ContextExecutor {
override fun invoke(context: Context) {
MarkDownWindow.show(context, null, item.readme)
}
}
class PermissionEvent(
val permissions: List<String>,
val callback: PublishSubject<Boolean>
) : ViewEvent()
) : ViewEvent(), ActivityExecutor {
class BackPressEvent : ViewEvent()
override fun invoke(activity: BaseActivity) =
activity.withPermissions(*permissions.toTypedArray()) {
onSuccess {
callback.onNext(true)
}
onFailure {
callback.onNext(false)
callback.onError(SecurityException("User refused permissions"))
}
}
}
class BackPressEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseActivity) {
activity.onBackPressed()
}
}
class DieEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseActivity) {
activity.finish()
}
}
class DieEvent : ViewEvent()
class RecreateEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseActivity) {
activity.recreate()
}
}
class RequestFileEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseActivity) {
Intent(Intent.ACTION_GET_CONTENT)
.setType("*/*")
.addCategory(Intent.CATEGORY_OPENABLE)
.also { activity.startActivityForResult(it, REQUEST_CODE) }
}
companion object {
private const val REQUEST_CODE = 10
fun resolve(requestCode: Int, resultCode: Int, data: Intent?) = data
?.takeIf { resultCode == Activity.RESULT_OK }
?.takeIf { requestCode == REQUEST_CODE }
?.data
}
}
package com.topjohnwu.magisk.model.events.dialog
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.model.events.ActivityExecutor
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.core.utils.BiometricHelper
class BiometricDialog(
builder: Builder.() -> Unit
) : ViewEvent(), ActivityExecutor {
private var listenerOnFailure: GenericDialogListener = {}
private var listenerOnSuccess: GenericDialogListener = {}
init {
builder(Builder())
}
override fun invoke(activity: BaseActivity) {
BiometricHelper.authenticate(
activity,
onError = listenerOnFailure,
onSuccess = listenerOnSuccess
)
}
inner class Builder internal constructor() {
fun onFailure(listener: GenericDialogListener) {
listenerOnFailure = listener
}
fun onSuccess(listener: GenericDialogListener) {
listenerOnSuccess = listener
}
}
}
package com.topjohnwu.magisk.model.events.dialog
import android.app.Activity
import androidx.appcompat.app.AppCompatDelegate
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.model.events.ActivityExecutor
import com.topjohnwu.magisk.view.MagiskDialog
import java.lang.ref.WeakReference
class DarkThemeDialog : DialogEvent(), ActivityExecutor {
private var activity: WeakReference<Activity>? = null
override fun invoke(activity: BaseActivity) {
this.activity = WeakReference(activity)
}
override fun build(dialog: MagiskDialog) {
dialog.applyTitle(R.string.settings_dark_mode_title)
.applyMessage(R.string.settings_dark_mode_message)
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = R.string.settings_dark_mode_light
icon = R.drawable.ic_day
onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_NO) }
}
.applyButton(MagiskDialog.ButtonType.NEUTRAL) {
titleRes = R.string.settings_dark_mode_system
icon = R.drawable.ic_day_night
onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) }
}
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = R.string.settings_dark_mode_dark
icon = R.drawable.ic_night
onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_YES) }
}
.onDismiss {
activity?.clear()
activity = null
}
}
private fun selectTheme(mode: Int) {
Config.darkThemeExtended = mode
activity?.get()?.recreate()
}
}
package com.topjohnwu.magisk.model.events.dialog
import android.content.Context
import com.topjohnwu.magisk.model.events.ContextExecutor
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.view.MagiskDialog
abstract class DialogEvent : ViewEvent(), ContextExecutor {
protected lateinit var dialog: MagiskDialog
override fun invoke(context: Context) {
dialog = MagiskDialog(context).apply(this::build).reveal()
}
abstract fun build(dialog: MagiskDialog)
}
typealias GenericDialogListener = () -> Unit
\ No newline at end of file
package com.topjohnwu.magisk.model.events.dialog
import android.content.DialogInterface
import android.widget.Toast
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.extensions.reboot
import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.internal.UiThreadHandler
import org.koin.core.KoinComponent
class EnvFixDialog : DialogEvent() {
override fun build(dialog: MagiskDialog) = dialog
.applyTitle(R.string.env_fix_title)
.applyMessage(R.string.env_fix_msg)
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = R.string.yes
preventDismiss = true
onClick {
dialog.applyTitle(R.string.setup_title)
.applyMessage(R.string.setup_msg)
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
title = ""
}
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
title = ""
}
.cancellable(false)
fixEnv(it)
}
}
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = android.R.string.no
}
.let { Unit }
private fun fixEnv(dialog: DialogInterface) {
object : MagiskInstaller(), KoinComponent {
override fun operations() = fixEnv()
override fun onResult(success: Boolean) {
dialog.dismiss()
Utils.toast(
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
Toast.LENGTH_LONG
)
if (success)
UiThreadHandler.handler.postDelayed({ reboot() }, 5000)
}
}.exec()
}
}
package com.topjohnwu.magisk.model.events.dialog
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.extensions.res
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.MarkDownWindow
class ManagerInstallDialog : DialogEvent() {
override fun build(dialog: MagiskDialog) {
with(dialog) {
val subject = DownloadSubject.Manager(Configuration.APK.Upgrade)
applyTitle(R.string.repo_install_title.res(R.string.app_name.res()))
applyMessage(R.string.repo_install_msg.res(subject.title))
setCancelable(true)
applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = R.string.install
onClick { DownloadService(context) { this.subject = subject } }
}
if (Info.remote.app.note.isEmpty()) return
applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = R.string.app_changelog
onClick { MarkDownWindow.show(context, null, Info.remote.app.note) }
}
}
}
}
package com.topjohnwu.magisk.model.events.dialog
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.view.MagiskDialog
class ModuleInstallDialog(private val item: Repo) : DialogEvent() {
override fun build(dialog: MagiskDialog) {
with(dialog) {
fun download(install: Boolean) = DownloadService(context) {
val config = if (install) Configuration.Flash.Primary else Configuration.Download
subject = DownloadSubject.Module(item, config)
}
applyTitle(context.getString(R.string.repo_install_title, item.name))
.applyMessage(context.getString(R.string.repo_install_msg, item.downloadFilename))
.cancellable(true)
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = R.string.install
icon = R.drawable.ic_install
onClick { download(true) }
}
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = R.string.download
icon = R.drawable.ic_download_md2
onClick { download(false) }
}
.reveal()
}
}
}
package com.topjohnwu.magisk.model.events.dialog
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.view.MagiskDialog
class SuperuserRevokeDialog(
builder: Builder.() -> Unit
) : DialogEvent() {
private val callbacks = Builder().apply(builder)
override fun build(dialog: MagiskDialog) {
dialog.applyTitle(R.string.su_revoke_title)
.applyMessage(R.string.su_revoke_msg, callbacks.appName)
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = R.string.yes
onClick { callbacks.listenerOnSuccess() }
}
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = android.R.string.no
}
}
inner class Builder internal constructor() {
var appName: String = ""
internal var listenerOnSuccess: GenericDialogListener = {}
fun onSuccess(listener: GenericDialogListener) {
listenerOnSuccess = listener
}
}
}
\ No newline at end of file
package com.topjohnwu.magisk.model.events.dialog
import android.widget.Toast
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.Shell
class UninstallDialog : DialogEvent() {
override fun build(dialog: MagiskDialog) {
dialog.applyTitle(R.string.uninstall_magisk_title)
.applyMessage(R.string.uninstall_magisk_msg)
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = R.string.restore_img
preventDismiss = true
onClick { restore(dialog) }
}
if (Info.remote.uninstaller.link.isNotEmpty()) {
dialog.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = R.string.complete_uninstall
onClick { completeUninstall() }
}
}
}
private fun restore(dialog: MagiskDialog) {
dialog.applyTitle(R.string.restore_img)
.applyMessage(R.string.restore_img_msg)
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
title = ""
}
.cancellable(false)
Shell.su("restore_imgs").submit { result ->
dialog.dismiss()
if (result.isSuccess) {
Utils.toast(R.string.restore_done, Toast.LENGTH_SHORT)
} else {
Utils.toast(R.string.restore_fail, Toast.LENGTH_LONG)
}
}
}
private fun completeUninstall() {
DownloadService(dialog.context) {
subject = DownloadSubject.Magisk(Configuration.Uninstall)
}
}
}
......@@ -3,8 +3,8 @@ package com.topjohnwu.magisk.model.flash
import android.content.Context
import android.net.Uri
import androidx.core.os.postDelayed
import com.topjohnwu.magisk.core.tasks.FlashZip
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.tasks.FlashZip
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler
......@@ -58,4 +58,4 @@ sealed class Flashing(
}
}
}
\ No newline at end of file
}
package com.topjohnwu.magisk.model.flash
import android.net.Uri
import com.topjohnwu.magisk.tasks.MagiskInstaller
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.superuser.Shell
sealed class Patching(
......@@ -28,8 +28,7 @@ sealed class Patching(
logs: MutableList<String>,
resultListener: FlashResultListener
) : Patching(file, console, logs, resultListener) {
override fun operations() =
extractZip() && handleFile(uri) && patchBoot() && storeBoot()
override fun operations() = doPatchFile(uri)
}
class SecondSlot(
......@@ -38,8 +37,7 @@ sealed class Patching(
logs: MutableList<String>,
resultListener: FlashResultListener
) : Patching(file, console, logs, resultListener) {
override fun operations() =
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
override fun operations() = secondSlot()
}
class Direct(
......@@ -48,8 +46,7 @@ sealed class Patching(
logs: MutableList<String>,
resultListener: FlashResultListener
) : Patching(file, console, logs, resultListener) {
override fun operations() =
findImage() && extractZip() && patchBoot() && flashBoot()
override fun operations() = direct()
}
}
\ No newline at end of file
}
......@@ -4,7 +4,10 @@ import android.os.Bundle
import androidx.annotation.AnimRes
import androidx.annotation.AnimatorRes
import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.model.events.ActivityExecutor
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.ui.base.CompatActivity
import kotlin.reflect.KClass
@DslMarker
......@@ -14,12 +17,17 @@ class MagiskNavigationEvent(
val navDirections: MagiskNavDirectionsBuilder,
val navOptions: MagiskNavOptions,
val animOptions: MagiskAnimBuilder
) : ViewEvent() {
) : ViewEvent(), ActivityExecutor {
companion object {
operator fun invoke(builder: Builder.() -> Unit) = Builder().apply(builder).build()
}
override fun invoke(activity: BaseActivity) {
if (activity !is CompatActivity<*, *>) return
activity.navigation?.navigateTo(this)
}
@NavigationDslMarker
class Builder {
......@@ -83,4 +91,4 @@ class MagiskAnimBuilder {
var popExit = 0
val anySet: Boolean get() = enter != 0 || exit != 0 || popEnter != 0 || popExit != 0
}
\ No newline at end of file
}
package com.topjohnwu.magisk.model.navigation
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment
import android.content.Context
import android.content.Intent
import android.os.Build
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.ui.hide.HideFragment
import com.topjohnwu.magisk.ui.home.HomeFragment
import com.topjohnwu.magisk.ui.install.InstallFragment
import com.topjohnwu.magisk.ui.log.LogFragment
import com.topjohnwu.magisk.ui.module.ModulesFragment
import com.topjohnwu.magisk.ui.module.ReposFragment
import com.topjohnwu.magisk.ui.module.ModuleFragment
import com.topjohnwu.magisk.ui.safetynet.SafetynetFragment
import com.topjohnwu.magisk.ui.settings.SettingsFragment
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment
import com.topjohnwu.magisk.ui.theme.ThemeFragment
object Navigation {
fun home() = MagiskNavigationEvent {
navDirections { destination = HomeFragment::class }
navOptions { popUpTo = HomeFragment::class }
navDirections {
destination = HomeFragment::class
}
navOptions {
popUpTo = HomeFragment::class
}
}
fun superuser() = MagiskNavigationEvent {
navDirections { destination = SuperuserFragment::class }
navDirections {
destination = SuperuserFragment::class
}
}
fun modules() = MagiskNavigationEvent {
navDirections { destination = ModulesFragment::class }
navDirections {
destination = ModuleFragment::class
}
}
fun repos() = MagiskNavigationEvent {
navDirections { destination = ReposFragment::class }
fun hide() = MagiskNavigationEvent {
navDirections {
destination = HideFragment::class
}
}
fun hide() = MagiskNavigationEvent {
navDirections { destination = MagiskHideFragment::class }
fun safetynet() = MagiskNavigationEvent {
navDirections { destination = SafetynetFragment::class }
}
fun log() = MagiskNavigationEvent {
navDirections { destination = LogFragment::class }
navDirections {
destination = LogFragment::class
}
}
fun settings() = MagiskNavigationEvent {
navDirections { destination = SettingsFragment::class }
navDirections {
destination = SettingsFragment::class
}
}
fun install() = MagiskNavigationEvent {
navDirections { destination = InstallFragment::class }
}
fun theme() = MagiskNavigationEvent {
navDirections { destination = ThemeFragment::class }
}
fun fromSection(section: String) = when (section) {
"superuser" -> superuser()
"modules" -> modules()
"downloads" -> repos()
"magiskhide" -> hide()
"log" -> log()
"settings" -> settings()
else -> home()
}
// redesign starts here
fun start(launchIntent: Intent, context: Context) {
context.intent<MainActivity>()
.putExtra(
Const.Key.OPEN_SECTION, launchIntent.getStringExtra(
Const.Key.OPEN_SECTION))
.putExtra(
Const.Key.OPEN_SETTINGS,
launchIntent.action == ACTION_APPLICATION_PREFERENCES
)
.also { context.startActivity(it) }
}
object Main {
const val OPEN_NAV = 1
}
private val ACTION_APPLICATION_PREFERENCES
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Intent.ACTION_APPLICATION_PREFERENCES
} else {
"cannot be null, cannot be empty"
}
}
......@@ -8,6 +8,4 @@ interface Navigator {
//TODO Elevate Fragment to MagiskFragment<*,*> once everything is on board with it
val baseFragments: List<KClass<out Fragment>>
fun navigateTo(event: MagiskNavigationEvent)
}
\ No newline at end of file
}
package com.topjohnwu.magisk.model.zip
import com.topjohnwu.magisk.extensions.forEach
import com.topjohnwu.magisk.extensions.withStreams
import com.topjohnwu.superuser.io.SuFile
import java.io.File
import java.util.zip.ZipInputStream
class Zip private constructor(private val values: Builder) {
companion object {
operator fun invoke(builder: Builder.() -> Unit): Zip {
return Zip(Builder().apply(builder))
}
}
class Builder {
lateinit var zip: File
lateinit var destination: File
var excludeDirs = true
}
data class Path(val path: String, val pullFromDir: Boolean = true)
fun unzip(vararg paths: Pair<String, Boolean>) =
unzip(*paths.map { Path(it.first, it.second) }.toTypedArray())
@Suppress("RedundantLambdaArrow")
fun unzip(vararg paths: Path) {
ensureRequiredParams()
values.zip.zipStream().use {
it.forEach { e ->
val currentPath = paths.firstOrNull { e.name.startsWith(it.path) }
val isDirectory = values.excludeDirs && e.isDirectory
if (currentPath == null || isDirectory) {
// Ignore directories, only create files
return@forEach
}
val name = if (currentPath.pullFromDir) {
e.name.substring(e.name.lastIndexOf('/') + 1)
} else {
e.name
}
val out = File(values.destination, name)
.ensureExists()
.outputStream()
//.suOutputStream()
withStreams(it, out) { reader, writer ->
reader.copyTo(writer)
}
}
}
}
private fun ensureRequiredParams() {
if (!values.zip.exists()) {
throw RuntimeException("Zip file does not exist")
}
}
private fun File.ensureExists() =
if ((!parentFile.exists() && !parentFile.mkdirs()) || parentFile is SuFile) {
SuFile(parentFile, name).apply { parentFile.mkdirs() }
} else {
this
}
private fun File.zipStream() = ZipInputStream(inputStream())
}
\ No newline at end of file
package com.topjohnwu.magisk.ui
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.core.view.GravityCompat
import android.view.MenuItem
import android.view.View
import android.view.ViewTreeObserver
import android.view.WindowManager
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.Insets
import androidx.core.view.setPadding
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import com.google.android.material.card.MaterialCardView
import com.ncapdevi.fragnav.FragNavController
import com.ncapdevi.fragnav.FragNavTransactionOptions
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Const.Key.OPEN_SECTION
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.ActivityMainBinding
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.snackbar
import com.topjohnwu.magisk.intent
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
import com.topjohnwu.magisk.extensions.startAnimations
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.model.navigation.Navigator
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment
import com.topjohnwu.magisk.ui.base.CompatActivity
import com.topjohnwu.magisk.ui.base.CompatNavigationDelegate
import com.topjohnwu.magisk.ui.home.HomeFragment
import com.topjohnwu.magisk.ui.log.LogFragment
import com.topjohnwu.magisk.ui.module.ModulesFragment
import com.topjohnwu.magisk.ui.module.ReposFragment
import com.topjohnwu.magisk.ui.settings.SettingsFragment
import com.topjohnwu.magisk.ui.module.ModuleFragment
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.HideBottomViewOnScrollBehavior
import com.topjohnwu.magisk.utils.HideTopViewOnScrollBehavior
import com.topjohnwu.magisk.utils.HideableBehavior
import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
import kotlin.reflect.KClass
open class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>(), Navigator,
FragNavController.RootFragmentListener, FragNavController.TransactionListener {
open class MainActivity : CompatActivity<MainViewModel, ActivityMainMd2Binding>(),
FragNavController.TransactionListener {
override val layoutRes: Int = R.layout.activity_main
override val viewModel: MainViewModel by viewModel()
private val navHostId: Int = R.id.main_nav_host
private val defaultPosition: Int = 0
override val layoutRes = R.layout.activity_main_md2
override val viewModel by viewModel<MainViewModel>()
override val navHost: Int = R.id.main_nav_host
private val navigationController by lazy {
FragNavController(supportFragmentManager, navHostId)
}
private val isRootFragment get() =
navigationController.currentStackIndex != defaultPosition
override val navigation by lazy { CompatNavigationDelegate(this, this) }
override val baseFragments: List<KClass<out Fragment>> = listOf(
HomeFragment::class,
SuperuserFragment::class,
MagiskHideFragment::class,
ModulesFragment::class,
ReposFragment::class,
LogFragment::class,
SettingsFragment::class
ModuleFragment::class,
SuperuserFragment::class
)
override fun onCreate(savedInstanceState: Bundle?) {
if (!SplashActivity.DONE) {
startActivity(intent<SplashActivity>())
finish()
}
//This temporarily fixes unwanted feature of BottomNavigationView - where the view applies
//padding on itself given insets are not consumed beforehand. Unfortunately the listener
//implementation doesn't favor us against the design library, so on re-create it's often given
//upper hand.
private val navObserver = ViewTreeObserver.OnGlobalLayoutListener {
binding.mainNavigation.setPadding(0)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Info.env.isUnsupported && !viewModel.shownUnsupportedDialog) {
viewModel.shownUnsupportedDialog = true
AlertDialog.Builder(this)
.setTitle(R.string.unsupport_magisk_title)
.setMessage(getString(R.string.unsupport_magisk_msg, Const.Version.MIN_VERSION))
.setPositiveButton(android.R.string.ok, null)
.show()
}
navigationController.apply {
rootFragmentListener = this@MainActivity
transactionListener = this@MainActivity
initialize(defaultPosition, savedInstanceState)
}
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
checkHideSection()
setSupportActionBar(binding.mainInclude.mainToolbar)
setSupportActionBar(binding.mainToolbar)
viewModel.isConnected.addOnPropertyChangedCallback {
checkHideSection()
binding.mainToolbarWrapper.updateLayoutParams<CoordinatorLayout.LayoutParams> {
behavior = HideTopViewOnScrollBehavior<MaterialCardView>()
}
if (savedInstanceState == null) {
intent.getStringExtra(OPEN_SECTION)?.let {
onEventDispatched(Navigation.fromSection(it))
}
binding.mainBottomBar.updateLayoutParams<CoordinatorLayout.LayoutParams> {
behavior = HideBottomViewOnScrollBehavior<MaterialCardView>()
}
binding.mainNavigation.setOnNavigationItemSelectedListener {
when (it.itemId) {
R.id.homeFragment -> Navigation.home()
R.id.modulesFragment -> Navigation.modules()
R.id.superuserFragment -> Navigation.superuser()
else -> throw NotImplementedError("Id ${it.itemId} is not defined as selectable")
}.dispatchOnSelf()
true
}
binding.mainNavigation.setOnNavigationItemReselectedListener {
navigation.onReselected()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
navigationController.onSaveInstanceState(outState)
}
override fun setTitle(title: CharSequence?) {
supportActionBar?.title = title
}
override fun setTitle(titleId: Int) {
supportActionBar?.setTitle(titleId)
}
binding.mainNavigation.viewTreeObserver.addOnGlobalLayoutListener(navObserver)
override fun onBackPressed() {
if (binding.drawerLayout.isDrawerOpen(binding.navView)) {
binding.drawerLayout.closeDrawer(binding.navView)
} else {
val fragment = navigationController.currentFrag as? BaseFragment<*, *>
if (intent.getBooleanExtra(Const.Key.OPEN_SETTINGS, false)) {
Navigation.settings().dispatchOnSelf()
}
if (fragment?.onBackPressed() == true) {
return
}
if (savedInstanceState != null) {
onTabTransaction(null, -1)
try {
navigationController.popFragment()
} catch (e: UnsupportedOperationException) {
when {
isRootFragment -> {
val options = FragNavTransactionOptions.newBuilder()
.transition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
.build()
navigationController.switchTab(defaultPosition, options)
}
else -> super.onBackPressed()
}
if (!navigation.isRoot) {
requestNavigationHidden()
}
}
}
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is SnackbarEvent -> snackbar(snackbarView, event.message(this), event.length, event.f)
is BackPressEvent -> onBackPressed()
is MagiskNavigationEvent -> navigateTo(event)
is ViewActionEvent -> event.action(this)
is PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) {
onSuccess { event.callback.onNext(true) }
onFailure {
event.callback.onNext(false)
event.callback.onError(SecurityException("User refused permissions"))
}
}
override fun onResume() {
super.onResume()
binding.mainNavigation.menu.apply {
val isRoot = Shell.rootAccess()
findItem(R.id.modulesFragment)?.isEnabled = isRoot
findItem(R.id.superuserFragment)?.isEnabled = isRoot
}
}
override fun onSimpleEventDispatched(event: Int) {
super.onSimpleEventDispatched(event)
when (event) {
Navigation.Main.OPEN_NAV -> openNav()
}
override fun onDestroy() {
binding.mainNavigation.viewTreeObserver.removeOnGlobalLayoutListener(navObserver)
super.onDestroy()
}
private fun openNav() = binding.drawerLayout.openDrawer(GravityCompat.START)
private fun checkHideSection() {
val menu = binding.navView.menu
menu.findItem(R.id.magiskHideFragment).isVisible = Info.env.isActive && Info.env.magiskHide
menu.findItem(R.id.modulesFragment).isVisible = Info.env.isActive
menu.findItem(R.id.reposFragment).isVisible = Info.isConnected.value && Info.env.isActive
menu.findItem(R.id.logFragment).isVisible = Info.env.isActive
menu.findItem(R.id.superuserFragment).isVisible = Utils.showSuperUser()
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> onBackPressed()
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) =
customAnimations(options.enter, options.exit, options.popEnter, options.popExit).apply {
if (!options.anySet) {
transition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
}
}
override fun onTabTransaction(fragment: Fragment?, index: Int) =
onFragmentTransaction(fragment, FragNavController.TransactionType.PUSH)
override val numberOfRootFragments: Int get() = baseFragments.size
override fun onFragmentTransaction(
fragment: Fragment?,
transactionType: FragNavController.TransactionType
) {
setDisplayHomeAsUpEnabled(!navigation.isRoot)
requestNavigationHidden(!navigation.isRoot)
}
override fun getRootFragment(index: Int) = baseFragments[index].java.newInstance()
override fun peekSystemWindowInsets(insets: Insets) {
viewModel.insets.value = insets
}
override fun onTabTransaction(fragment: Fragment?, index: Int) {
val fragmentId = when (fragment) {
is HomeFragment -> R.id.magiskFragment
is SuperuserFragment -> R.id.superuserFragment
is MagiskHideFragment -> R.id.magiskHideFragment
is ModulesFragment -> R.id.modulesFragment
is ReposFragment -> R.id.reposFragment
is LogFragment -> R.id.logFragment
is SettingsFragment -> R.id.settings
else -> return
private fun setDisplayHomeAsUpEnabled(isEnabled: Boolean) {
binding.mainToolbar.startAnimations()
when {
isEnabled -> binding.mainToolbar.setNavigationIcon(R.drawable.ic_back_md2)
else -> binding.mainToolbar.navigationIcon = null
}
binding.navView.setCheckedItem(fragmentId)
}
override fun navigateTo(event: MagiskNavigationEvent) {
val directions = event.navDirections
@Suppress("UNCHECKED_CAST")
internal fun requestNavigationHidden(hide: Boolean = true) {
val topView = binding.mainToolbarWrapper
val bottomView = binding.mainBottomBar
navigationController.defaultTransactionOptions = FragNavTransactionOptions.newBuilder()
.customAnimations(event.animOptions)
.build()
val topParams = topView.layoutParams as? CoordinatorLayout.LayoutParams
val bottomParams = bottomView.layoutParams as? CoordinatorLayout.LayoutParams
navigationController.currentStack
?.indexOfFirst { it.javaClass == event.navOptions.popUpTo }
?.let { if (it == -1) null else it } // invalidate if class is not found
?.let { if (event.navOptions.inclusive) it + 1 else it }
?.let { navigationController.popFragments(it) }
val topBehavior = topParams?.behavior as? HideableBehavior<View>
val bottomBehavior = bottomParams?.behavior as? HideableBehavior<View>
when (directions.isActivity) {
true -> navigateToActivity(event)
else -> navigateToFragment(event)
}
topBehavior?.setHidden(topView, hide = false, lockState = false)
bottomBehavior?.setHidden(bottomView, hide, hide)
}
private fun navigateToActivity(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination?.java ?: let {
Timber.e("Cannot navigate to null destination")
return
}
val options = event.navOptions
Intent(this, destination)
.putExtras(event.navDirections.args)
.apply {
if (options.singleTop) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
if (options.clearTask) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
.let { startActivity(it) }
fun invalidateToolbar() {
//binding.mainToolbar.startAnimations()
binding.mainToolbar.invalidate()
}
private fun navigateToFragment(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination?.java ?: let {
Timber.e("Cannot navigate to null destination")
return
}
when (val index = baseFragments.indexOfFirst { it.java.name == destination.name }) {
-1 -> destination.newInstance()
.apply { arguments = event.navDirections.args }
.let { navigationController.pushFragment(it) }
// When it's desired that fragments of same class are put on top of one another edit this
else -> navigationController.switchTab(index)
}
}
override fun onFragmentTransaction(
fragment: Fragment?,
transactionType: FragNavController.TransactionType
) = Unit
}
package com.topjohnwu.magisk.ui
import android.view.MenuItem
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.ui.base.BaseViewModel
class MainViewModel : BaseViewModel() {
var shownUnsupportedDialog = false
fun navPressed() = Navigation.Main.OPEN_NAV.publish()
fun navigationItemPressed(item: MenuItem): Boolean {
when (item.itemId) {
R.id.magiskFragment -> Navigation.home()
R.id.superuserFragment -> Navigation.superuser()
R.id.magiskHideFragment -> Navigation.hide()
R.id.modulesFragment -> Navigation.modules()
R.id.reposFragment -> Navigation.repos()
R.id.logFragment -> Navigation.log()
R.id.settings -> Navigation.settings()
else -> null
}?.publish()?.let { return@navigationItemPressed true }
return false
}
}
class MainViewModel : BaseViewModel()
package com.topjohnwu.magisk.ui
interface ReselectionTarget {
fun onReselected()
}
package com.topjohnwu.magisk.ui.base
import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.model.events.EventHandler
abstract class BaseUIActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
BaseActivity(), EventHandler {
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
abstract val viewModel: ViewModel
protected open val themeRes: Int = R.style.MagiskTheme
open val snackbarView get() = binding.root
init {
val theme = Config.darkThemeExtended
AppCompatDelegate.setDefaultNightMode(theme)
}
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(themeRes)
super.onCreate(savedInstanceState)
viewModel.viewEvents.observe(this, viewEventObserver)
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).apply {
setVariable(BR.viewModel, viewModel)
lifecycleOwner = this@BaseUIActivity
}
}
}
package com.topjohnwu.magisk.base
package com.topjohnwu.magisk.ui.base
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.model.events.EventHandler
import com.topjohnwu.magisk.model.events.ViewEvent
abstract class BaseFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
abstract class BaseUIFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
Fragment(), EventHandler {
protected val activity get() = requireActivity() as BaseActivity<*, *>
protected val activity get() = requireActivity() as BaseUIActivity<*, *>
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
protected abstract val viewModel: ViewModel
abstract val viewModel: ViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -33,13 +31,12 @@ abstract class BaseFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding
): View? {
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).apply {
setVariable(BR.viewModel, viewModel)
lifecycleOwner = this@BaseFragment
lifecycleOwner = this@BaseUIFragment
}
return binding.root
}
@CallSuper
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
activity.onEventDispatched(event)
......
package com.topjohnwu.magisk.ui.base
import androidx.annotation.CallSuper
import androidx.core.graphics.Insets
import androidx.databinding.Bindable
import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.model.observer.Observer
import com.topjohnwu.magisk.utils.KObservableField
import io.reactivex.*
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.subjects.PublishSubject
import org.koin.core.KoinComponent
import androidx.databinding.Observable as BindingObservable
abstract class BaseViewModel(
initialState: State = State.LOADING
) : ViewModel(), BindingObservable, KoinComponent {
enum class State {
LOADED, LOADING, LOADING_FAILED
}
val loading @Bindable get() = state == State.LOADING
val loaded @Bindable get() = state == State.LOADED
val loadingFailed @Bindable get() = state == State.LOADING_FAILED
val isConnected = Observer(Info.isConnected) { Info.isConnected.value }
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
val insets = KObservableField(Insets.NONE)
var state: State = initialState
set(value) {
field = value
notifyStateChanged()
}
private val disposables = CompositeDisposable()
private val _viewEvents = MutableLiveData<ViewEvent>()
private var runningTask: Disposable? = null
private val refreshCallback = object : androidx.databinding.Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: androidx.databinding.Observable?, propertyId: Int) {
requestRefresh()
}
}
init {
isConnected.addOnPropertyChangedCallback(refreshCallback)
}
/** This should probably never be called manually, it's called manually via delegate. */
@Synchronized
fun requestRefresh() {
if (runningTask?.isDisposed?.not() == true) {
return
}
runningTask = refresh()
}
protected open fun refresh(): Disposable? = null
open fun notifyStateChanged() {
notifyPropertyChanged(BR.loading)
notifyPropertyChanged(BR.loaded)
notifyPropertyChanged(BR.loadingFailed)
}
@CallSuper
override fun onCleared() {
isConnected.removeOnPropertyChangedCallback(refreshCallback)
disposables.clear()
super.onCleared()
}
fun withView(action: BaseActivity.() -> Unit) {
ViewActionEvent(action).publish()
}
fun withPermissions(vararg permissions: String): Observable<Boolean> {
val subject = PublishSubject.create<Boolean>()
return subject.doOnSubscribeUi { PermissionEvent(permissions.toList(), subject).publish() }
}
fun back() = BackPressEvent().publish()
fun <Event : ViewEvent> Event.publish() {
_viewEvents.postValue(this)
}
fun Int.publish() {
_viewEvents.postValue(SimpleViewEvent(this))
}
fun Disposable.add() {
disposables.add(this)
}
// The following is copied from androidx.databinding.BaseObservable
@Transient
private var callbacks: PropertyChangeRegistry? = null
@Synchronized
override fun addOnPropertyChangedCallback(callback: BindingObservable.OnPropertyChangedCallback) {
if (callbacks == null) {
callbacks = PropertyChangeRegistry()
}
callbacks?.add(callback)
}
@Synchronized
override fun removeOnPropertyChangedCallback(callback: BindingObservable.OnPropertyChangedCallback) {
callbacks?.remove(callback)
}
/**
* Notifies listeners that all properties of this instance have changed.
*/
@Synchronized
fun notifyChange() {
callbacks?.notifyCallbacks(this, 0, null)
}
/**
* Notifies listeners that a specific property has changed. The getter for the property
* that changes should be marked with [androidx.databinding.Bindable] to generate a field in
* `BR` to be used as `fieldId`.
*
* @param fieldId The generated BR id for the Bindable field.
*/
fun notifyPropertyChanged(fieldId: Int) {
callbacks?.notifyCallbacks(this, fieldId, null)
}
//region Rx
protected fun <T> Observable<T>.applyViewModel(viewModel: BaseViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state =
State.LOADING
}
.doOnError { viewModel.state =
State.LOADING_FAILED
}
.doOnNext { if (allowFinishing) viewModel.state =
State.LOADED
}
protected fun <T> Single<T>.applyViewModel(viewModel: BaseViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state =
State.LOADING
}
.doOnError { viewModel.state =
State.LOADING_FAILED
}
.doOnSuccess { if (allowFinishing) viewModel.state =
State.LOADED
}
protected fun <T> Maybe<T>.applyViewModel(viewModel: BaseViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state =
State.LOADING
}
.doOnError { viewModel.state =
State.LOADING_FAILED
}
.doOnComplete { if (allowFinishing) viewModel.state =
State.LOADED
}
.doOnSuccess { if (allowFinishing) viewModel.state =
State.LOADED
}
protected fun <T> Flowable<T>.applyViewModel(viewModel: BaseViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state =
State.LOADING
}
.doOnError { viewModel.state =
State.LOADING_FAILED
}
.doOnNext { if (allowFinishing) viewModel.state =
State.LOADED
}
protected fun Completable.applyViewModel(viewModel: BaseViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state =
State.LOADING
}
.doOnError { viewModel.state =
State.LOADING_FAILED
}
.doOnComplete { if (allowFinishing) viewModel.state =
State.LOADED
}
//endregion
}
package com.topjohnwu.magisk.ui.base
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.core.content.getSystemService
import androidx.databinding.OnRebindCallback
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.extensions.snackbar
import com.topjohnwu.magisk.extensions.startAnimations
import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.model.navigation.Navigator
import com.topjohnwu.magisk.ui.theme.Theme
import kotlin.reflect.KClass
// TODO (diareuse): Merge into BaseUIActivity after all legacy UI is migrated
abstract class CompatActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
BaseUIActivity<ViewModel, Binding>(), CompatView<ViewModel>, Navigator {
override val themeRes = Theme.selected.themeRes
override val viewRoot: View get() = binding.root
override val navigation: CompatNavigationDelegate<CompatActivity<ViewModel, Binding>>? by lazy {
CompatNavigationDelegate(this)
}
override val baseFragments = listOf<KClass<out Fragment>>()
private val delegate by lazy { CompatDelegate(this) }
internal abstract val navHost: Int
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
navigation?.onActivityResult(requestCode, resultCode, data)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.addOnRebindCallback(object : OnRebindCallback<Binding>() {
override fun onPreBind(binding: Binding): Boolean {
(binding.root as? ViewGroup)?.startAnimations()
return super.onPreBind(binding)
}
})
delegate.onCreate()
navigation?.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
delegate.onResume()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
navigation?.onSaveInstanceState(outState)
}
override fun onEventDispatched(event: ViewEvent) {
delegate.onEventExecute(event, this)
when (event) {
is SnackbarEvent -> snackbar(snackbarView, event.message(this), event.length, event.f)
}
}
override fun onBackPressed() {
if (navigation?.onBackPressed()?.not() == true) {
super.onBackPressed()
}
}
protected fun ViewEvent.dispatchOnSelf() = onEventDispatched(this)
}
fun Activity.hideKeyboard() {
val view = currentFocus ?: return
getSystemService<InputMethodManager>()
?.hideSoftInputFromWindow(view.windowToken, 0)
view.clearFocus()
}
package com.topjohnwu.magisk.ui.base
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.model.events.ActivityExecutor
import com.topjohnwu.magisk.model.events.ContextExecutor
import com.topjohnwu.magisk.model.events.FragmentExecutor
import com.topjohnwu.magisk.model.events.ViewEvent
import timber.log.Timber
class CompatDelegate internal constructor(
private val view: CompatView<*>
) {
fun onCreate() {
ensureInsets()
}
fun onResume() {
view.viewModel.requestRefresh()
}
fun onEventExecute(event: ViewEvent, activity: BaseUIActivity<*, *>) {
(event as? ContextExecutor)?.invoke(activity)
(event as? ActivityExecutor)?.invoke(activity)
(event as? FragmentExecutor)?.let {
Timber.e("Cannot run ${FragmentExecutor::class.java.simpleName} in Activity. Consider adding ${ContextExecutor::class.java.simpleName} as fallback.")
}
}
fun onEventExecute(event: ViewEvent, fragment: Fragment) {
(event as? ContextExecutor)?.invoke(fragment.requireContext())
(event as? FragmentExecutor)?.invoke(fragment)
(event as? ActivityExecutor)?.invoke(fragment.requireActivity() as BaseUIActivity<*, *>)
}
private fun ensureInsets() {
ViewCompat.setOnApplyWindowInsetsListener(view.viewRoot) { _, insets ->
insets.asInsets()
.also { view.peekSystemWindowInsets(it) }
.let { view.consumeSystemWindowInsets(it) }
?.also { view.viewModel.insets.value = it }
?.subtractBy(insets) ?: insets
}
if (ViewCompat.isAttachedToWindow(view.viewRoot)) {
ViewCompat.requestApplyInsets(view.viewRoot)
} else {
view.viewRoot.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(v: View) = Unit
override fun onViewAttachedToWindow(v: View) {
ViewCompat.requestApplyInsets(v)
}
})
}
}
private fun WindowInsetsCompat.asInsets() = Insets.of(
systemWindowInsetLeft,
systemWindowInsetTop,
systemWindowInsetRight,
systemWindowInsetBottom
)
private fun Insets.subtractBy(insets: WindowInsetsCompat) = insets.replaceSystemWindowInsets(
insets.systemWindowInsetLeft - left,
insets.systemWindowInsetTop - top,
insets.systemWindowInsetRight - right,
insets.systemWindowInsetBottom - bottom
)
}
package com.topjohnwu.magisk.ui.base
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.databinding.OnRebindCallback
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.extensions.startAnimations
import com.topjohnwu.magisk.model.events.ViewEvent
// TODO (diareuse): Merge into BaseUIFragment after all legacy UI is migrated
abstract class CompatFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding>
: BaseUIFragment<ViewModel, Binding>(), CompatView<ViewModel> {
override val viewRoot: View get() = binding.root
override val navigation by lazy { compatActivity.navigation }
private val delegate by lazy { CompatDelegate(this) }
protected val compatActivity get() = requireActivity() as CompatActivity<*, *>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.addOnRebindCallback(object : OnRebindCallback<Binding>() {
override fun onPreBind(binding: Binding): Boolean {
this@CompatFragment.onPreBind(binding)
return true
}
})
delegate.onCreate()
}
override fun onResume() {
super.onResume()
delegate.onResume()
}
override fun onEventDispatched(event: ViewEvent) {
delegate.onEventExecute(event, this)
}
protected open fun onPreBind(binding: Binding) {
(binding.root as? ViewGroup)?.startAnimations()
}
protected fun ViewEvent.dispatchOnSelf() = delegate.onEventExecute(this, this@CompatFragment)
}
fun Fragment.hideKeyboard() {
activity?.hideKeyboard()
}
package com.topjohnwu.magisk.ui.base
import androidx.databinding.ViewDataBinding
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.FilterableDiffObservableList
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
import me.tatarka.bindingcollectionadapter2.ItemBinding
import me.tatarka.bindingcollectionadapter2.OnItemBind
inline fun <T : ComparableRvItem<*>> diffListOf(
vararg newItems: T
) = diffListOf(newItems.toList())
inline fun <T : ComparableRvItem<*>> diffListOf(
newItems: List<T>
) = DiffObservableList(object : DiffObservableList.Callback<T> {
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
}).also { it.update(newItems) }
inline fun <T : ComparableRvItem<*>> filterableListOf(
vararg newItems: T
) = FilterableDiffObservableList(object : DiffObservableList.Callback<T> {
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
}).also { it.update(newItems.toList()) }
fun <T : ComparableRvItem<*>> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
override fun onBindBinding(
binding: ViewDataBinding,
variableId: Int,
layoutRes: Int,
position: Int,
item: T
) {
super.onBindBinding(binding, variableId, layoutRes, position, item)
item.onBindingBound(binding)
}
}
inline fun <T : ComparableRvItem<*>> itemBindingOf(
crossinline body: (ItemBinding<*>) -> Unit = {}
) = OnItemBind<T> { itemBinding, _, item ->
item.bind(itemBinding)
body(itemBinding)
}
package com.topjohnwu.magisk.ui.base
import android.content.Intent
import android.os.Bundle
import com.ncapdevi.fragnav.FragNavController
import com.ncapdevi.fragnav.FragNavTransactionOptions
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
import com.topjohnwu.magisk.model.navigation.Navigator
import com.topjohnwu.magisk.ui.ReselectionTarget
import timber.log.Timber
class CompatNavigationDelegate<out Source>(
private val source: Source,
private val listener: FragNavController.TransactionListener? = null
) : FragNavController.RootFragmentListener where Source : CompatActivity<*, *>, Source : Navigator {
private val controller by lazy {
check(source.navHost != 0) { "Did you forget to override \"navHostId\"?" }
FragNavController(source.supportFragmentManager, source.navHost)
}
val isRoot get() = controller.isRootFragment
//region Listener
override val numberOfRootFragments: Int
get() = source.baseFragments.size
override fun getRootFragment(index: Int) =
source.baseFragments[index].java.newInstance()
//endregion
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
controller.currentFrag?.onActivityResult(requestCode, resultCode, data)
}
fun onCreate(savedInstanceState: Bundle?) = controller.run {
rootFragmentListener = this@CompatNavigationDelegate
transactionListener = listener
initialize(0, savedInstanceState)
}
fun onSaveInstanceState(outState: Bundle) =
controller.onSaveInstanceState(outState)
fun onReselected() {
(controller.currentFrag as? ReselectionTarget)?.onReselected()
}
fun onBackPressed(): Boolean {
val fragment = controller.currentFrag as? CompatFragment<*, *>
if (fragment?.onBackPressed() == true) {
return true
}
return runCatching { controller.popFragment() }.fold({ true }, { false })
}
// ---
fun navigateTo(event: MagiskNavigationEvent) {
val directions = event.navDirections
controller.defaultTransactionOptions = FragNavTransactionOptions.newBuilder()
.customAnimations(event.animOptions)
.build()
controller.currentStack
?.indexOfFirst { it.javaClass == event.navOptions.popUpTo }
?.takeIf { it != -1 } // invalidate if class is not found
?.let { if (event.navOptions.inclusive) it + 1 else it }
?.let { controller.popFragments(it) }
when (directions.isActivity) {
true -> navigateToActivity(event)
else -> navigateToFragment(event)
}
}
private fun navigateToActivity(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination?.java ?: let {
Timber.e("Cannot navigate to null destination")
return
}
val options = event.navOptions
Intent(source, destination)
.putExtras(event.navDirections.args)
.apply {
if (options.singleTop) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
if (options.clearTask) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
.let { source.startActivity(it) }
}
private fun navigateToFragment(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination ?: let {
Timber.e("Cannot navigate to null destination")
return
}
source.baseFragments
.indexOfFirst { it == destination }
.takeIf { it >= 0 }
?.let { controller.switchTab(it) } ?: destination.java.newInstance()
.also { it.arguments = event.navDirections.args }
.let { controller.pushFragment(it) }
}
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) =
apply {
if (!options.anySet) customAnimations(
R.anim.fragment_enter,
R.anim.fragment_exit,
R.anim.fragment_enter_pop,
R.anim.fragment_exit_pop
) else customAnimations(
options.enter,
options.exit,
options.popEnter,
options.popExit
)
}
}
package com.topjohnwu.magisk.ui.base
import android.view.View
import androidx.core.graphics.Insets
internal interface CompatView<ViewModel : BaseViewModel> {
val viewRoot: View
val viewModel: ViewModel
val navigation: CompatNavigationDelegate<*>?
fun peekSystemWindowInsets(insets: Insets) = Unit
fun consumeSystemWindowInsets(insets: Insets): Insets? = null
}
package com.topjohnwu.magisk.ui.base
import android.os.Handler
import android.os.Looper
interface Queryable {
val queryDelay: Long
val queryHandler: Handler
val queryRunnable: Runnable
fun submitQuery()
companion object {
fun impl(delay: Long = 1000L) = object : Queryable {
override val queryDelay = delay
override val queryHandler = Handler(Looper.getMainLooper())
override val queryRunnable = Runnable { TODO() }
override fun submitQuery() {}
}
}
}
package com.topjohnwu.magisk.ui.flash
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentFlashMd2Binding
import com.topjohnwu.magisk.ui.base.CompatFragment
import org.koin.androidx.viewmodel.ext.android.viewModel
class FlashFragment : CompatFragment<FlashViewModel, FragmentFlashMd2Binding>() {
override val layoutRes = R.layout.fragment_flash_md2
override val viewModel by viewModel<FlashViewModel>()
}
package com.topjohnwu.magisk.ui.flash
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.content.res.Resources
import android.net.Uri
import android.os.Handler
import androidx.core.os.postDelayed
import androidx.databinding.ObservableArrayList
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.model.flash.FlashResultListener
import com.topjohnwu.magisk.model.flash.Flashing
import com.topjohnwu.magisk.model.flash.Patching
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.superuser.Shell
import me.tatarka.bindingcollectionadapter2.ItemBinding
import java.io.File
import java.util.*
import com.topjohnwu.magisk.ui.base.BaseViewModel
class FlashViewModel(
action: String,
installer: Uri,
uri: Uri,
private val resources: Resources
) : BaseViewModel(), FlashResultListener {
val canShowReboot = Shell.rootAccess()
val showRestartTitle = KObservableField(false)
val behaviorText = KObservableField(resources.getString(R.string.flashing))
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@FlashViewModel)
}
private val outItems = ObservableArrayList<String>()
private val logItems = Collections.synchronizedList(mutableListOf<String>())
init {
outItems.sendUpdatesTo(items) { it.map { ConsoleRvItem(it) } }
outItems.copyNewInputInto(logItems)
state = State.LOADING
when (action) {
Const.Value.FLASH_ZIP -> Flashing
.Install(installer, outItems, logItems, this)
.exec()
Const.Value.UNINSTALL -> Flashing
.Uninstall(installer, outItems, logItems, this)
.exec()
Const.Value.FLASH_MAGISK -> Patching
.Direct(installer, outItems, logItems, this)
.exec()
Const.Value.FLASH_INACTIVE_SLOT -> Patching
.SecondSlot(installer, outItems, logItems, this)
.exec()
Const.Value.PATCH_FILE -> Patching
.File(installer, uri, outItems, logItems, this)
.exec()
}
}
override fun onResult(isSuccess: Boolean) {
state = if (isSuccess) State.LOADED else State.LOADING_FAILED
behaviorText.value = when {
isSuccess -> resources.getString(R.string.done)
else -> resources.getString(R.string.failure)
}
if (isSuccess) {
Handler().postDelayed(500) {
showRestartTitle.value = true
}
}
}
fun savePressed() = withPermissions(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE)
.map { now }
.map { it.toTime(timeFormatStandard) }
.map { Const.MAGISK_INSTALL_LOG_FILENAME.format(it) }
.map { File(Config.downloadDirectory, it) }
.map { file ->
file.bufferedWriter().use { writer ->
logItems.forEach {
writer.write(it)
writer.newLine()
}
}
file.path
}
.subscribeK { SnackbarEvent(it).publish() }
.add()
fun restartPressed() = reboot()
fun backPressed() = back()
}
\ No newline at end of file
class FlashViewModel : BaseViewModel()
package com.topjohnwu.magisk.ui.hide
import android.content.Context
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentHideMd2Binding
import com.topjohnwu.magisk.ui.base.CompatFragment
import com.topjohnwu.magisk.ui.base.hideKeyboard
import com.topjohnwu.magisk.utils.MotionRevealHelper
import org.koin.androidx.viewmodel.ext.android.viewModel
class HideFragment : CompatFragment<HideViewModel, FragmentHideMd2Binding>() {
override val layoutRes = R.layout.fragment_hide_md2
override val viewModel by viewModel<HideViewModel>()
private var isFilterVisible
get() = binding.hideFilter.isVisible
set(value) {
if (!value) hideKeyboard()
MotionRevealHelper.withViews(binding.hideFilter, binding.hideFilterToggle, value)
}
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onAttach(context: Context) {
super.onAttach(context)
activity.setTitle(R.string.magiskhide)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.hideFilterToggle.setOnClickListener {
isFilterVisible = true
}
binding.hideFilterInclude.hideFilterDone.setOnClickListener {
isFilterVisible = false
}
binding.hideContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState != RecyclerView.SCROLL_STATE_IDLE) hideKeyboard()
}
})
val lama = binding.hideContent.layoutManager ?: return
lama.isAutoMeasureEnabled = false
}
override fun onPreBind(binding: FragmentHideMd2Binding) = Unit
override fun onBackPressed(): Boolean {
if (isFilterVisible) {
isFilterVisible = false
return true
}
return super.onBackPressed()
}
// ---
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_hide_md2, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_focus_up -> binding.hideContent
.takeIf { (it.layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition() ?: 0 > 10 }
?.also { it.scrollToPosition(10) }
.let { binding.hideContent }
.also { it.post { it.smoothScrollToPosition(0) } }
}
return super.onOptionsItemSelected(item)
}
}
package com.topjohnwu.magisk.ui.hide
import android.content.pm.ApplicationInfo
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.toSingle
import com.topjohnwu.magisk.extensions.update
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
import com.topjohnwu.magisk.model.entity.recycler.HideRvItem
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.magisk.model.entity.ProcessHideApp
import com.topjohnwu.magisk.model.entity.StatefulProcess
import com.topjohnwu.magisk.model.entity.recycler.HideItem
import com.topjohnwu.magisk.model.entity.recycler.HideProcessItem
import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.Queryable
import com.topjohnwu.magisk.ui.base.filterableListOf
import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
import me.tatarka.bindingcollectionadapter2.OnItemBind
import timber.log.Timber
class HideViewModel(
private val magiskRepo: MagiskRepository,
rxBus: RxBus
) : BaseViewModel() {
val query = KObservableField("")
val isShowSystem = KObservableField(false)
private val allItems = mutableListOf<ComparableRvItem<*>>()
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@HideViewModel)
}
private val magiskRepo: MagiskRepository
) : BaseViewModel(), Queryable by Queryable.impl(1000) {
init {
rxBus.register<HideProcessEvent>()
.subscribeK { toggleItem(it.item) }
.add()
override val queryRunnable = Runnable { query() }
isShowSystem.addOnPropertyChangedCallback { query() }
query.addOnPropertyChangedCallback { query() }
var isShowSystem = false
@Bindable get
set(value) {
field = value
notifyPropertyChanged(BR.showSystem)
query()
}
refresh()
var query = ""
@Bindable get
set(value) {
field = value
notifyPropertyChanged(BR.query)
submitQuery()
}
val items = filterableListOf<HideItem>()
val itemBinding = itemBindingOf<HideItem> {
it.bindExtra(BR.viewModel, this)
}
fun refresh() {
// fetching this for every item is nonsensical, so we add .cache() so the response is all
// the same for every single mapped item, it only actually executes the whole thing the
// first time around.
val hideTargets = magiskRepo.fetchHideTargets().cache()
magiskRepo.fetchApps()
.flattenAsFlowable { it }
.map { HideRvItem(it, hideTargets.blockingGet()) }
.toList()
.map {
it.sortedWith(compareBy(
{ it.isHiddenState.value },
{ it.item.name.toLowerCase() },
{ it.packageName }
))
}
.doOnSuccess { allItems.update(it) }
.flatMap { queryRaw() }
.applyViewModel(this)
.subscribeK(onError = Timber::e) { items.update(it.first, it.second) }
.add()
val itemInternalBinding = itemBindingOf<HideProcessItem> {
it.bindExtra(BR.viewModel, this)
}
private fun query() = queryRaw()
.subscribeK { items.update(it.first, it.second) }
.add()
val isFilterExpanded = KObservableField(false)
private fun queryRaw(
showSystem: Boolean = isShowSystem.value,
query: String = this.query.value
) = allItems.toSingle()
.map { it.filterIsInstance<HideRvItem>() }
override fun refresh() = magiskRepo.fetchApps()
.map { it to magiskRepo.fetchHideTargets().blockingGet() }
.map { pair -> pair.first.map { mergeAppTargets(it, pair.second) } }
.flattenAsFlowable { it }
.filter {
it.item.name.contains(query, ignoreCase = true) ||
it.item.processes.any { it.contains(query, ignoreCase = true) }
}
.filter {
showSystem || (it.isHiddenState.value != IndeterminateState.UNCHECKED) ||
(it.item.info.flags and ApplicationInfo.FLAG_SYSTEM == 0)
}
.map { HideItem(it) }
.toList()
.map { it.sort() }
.map { it to items.calculateDiff(it) }
.applyViewModel(this)
.subscribeK {
items.update(it.first, it.second)
submitQuery()
}
private fun toggleItem(item: HideProcessRvItem) =
magiskRepo.toggleHide(item.isHidden.value, item.packageName, item.process)
// ---
private fun mergeAppTargets(a: HideAppInfo, ts: List<HideTarget>): ProcessHideApp {
val relevantTargets = ts.filter { it.packageName == a.info.packageName }
val packageName = a.info.packageName
val processes = a.processes
.map { StatefulProcess(it, packageName, relevantTargets.any { i -> it == i.process }) }
return ProcessHideApp(a, processes)
}
private fun List<HideItem>.sort() = compareByDescending<HideItem> { it.itemsChecked.value }
.thenBy { it.item.info.name.toLowerCase(currentLocale) }
.thenBy { it.item.info.info.packageName }
.let { sortedWith(it) }
// ---
override fun submitQuery() {
queryHandler.removeCallbacks(queryRunnable)
queryHandler.postDelayed(queryRunnable, queryDelay)
}
private fun query(
query: String = this.query,
showSystem: Boolean = isShowSystem
) = items.filter {
fun filterSystem(): Boolean {
return showSystem || it.item.info.info.flags and ApplicationInfo.FLAG_SYSTEM == 0
}
fun filterQuery(): Boolean {
val inName = it.item.info.name.contains(query, true)
val inPackage = it.item.info.info.packageName.contains(query, true)
val inProcesses = it.item.processes.any { it.name.contains(query, true) }
return inName || inPackage || inProcesses
}
filterSystem() && filterQuery()
}
// ---
fun toggleItem(item: HideProcessItem) = magiskRepo
.toggleHide(item.isHidden.value, item.item.packageName, item.item.name)
fun toggle(item: KObservableField<Boolean>) = item.toggle()
fun resetQuery() {
query = ""
}
fun hideFilter() {
isFilterExpanded.value = false
}
}
package com.topjohnwu.magisk.ui.hide
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.SearchView
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentMagiskHideBinding
import org.koin.androidx.viewmodel.ext.android.viewModel
class MagiskHideFragment : BaseFragment<HideViewModel, FragmentMagiskHideBinding>(),
SearchView.OnQueryTextListener {
override val layoutRes: Int = R.layout.fragment_magisk_hide
override val viewModel: HideViewModel by viewModel()
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.magiskhide)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_magiskhide, menu)
menu.apply {
val query = viewModel.query.value
val searchItem = menu.findItem(R.id.app_search)
val searchView = searchItem.actionView as? SearchView
searchView?.run {
setOnQueryTextListener(this@MagiskHideFragment)
setQuery(query, false)
}
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView?.isIconified = false
} else {
searchItem.collapseActionView()
searchView?.isIconified = true
}
val showSystem = Config.showSystemApp
findItem(R.id.show_system).isChecked = showSystem
viewModel.isShowSystem.value = showSystem
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.show_system) {
val showSystem = !item.isChecked
item.isChecked = showSystem
Config.showSystemApp = showSystem
viewModel.isShowSystem.value = showSystem
}
return true
}
override fun onQueryTextSubmit(query: String?): Boolean {
viewModel.query.value = query.orEmpty()
return false
}
override fun onQueryTextChange(query: String?): Boolean {
viewModel.query.value = query.orEmpty()
return false
}
}
package com.topjohnwu.magisk.ui.home
import android.content.Context
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Info
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.graphics.Insets
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.databinding.FragmentMagiskBinding
import com.topjohnwu.magisk.extensions.DynamicClassLoader
import com.topjohnwu.magisk.extensions.openUrl
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.utils.SafetyNetHelper
import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.magisk.view.dialogs.*
import com.topjohnwu.superuser.Shell
import dalvik.system.DexFile
import io.reactivex.Completable
import org.koin.android.ext.android.inject
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.ui.base.CompatFragment
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.io.File
import java.lang.reflect.InvocationHandler
class HomeFragment : BaseFragment<HomeViewModel, FragmentMagiskBinding>(),
SafetyNetHelper.Callback {
class HomeFragment : CompatFragment<HomeViewModel, FragmentHomeMd2Binding>() {
override val layoutRes: Int = R.layout.fragment_magisk
override val viewModel: HomeViewModel by viewModel()
override val layoutRes = R.layout.fragment_home_md2
override val viewModel by viewModel<HomeViewModel>()
private val magiskRepo: MagiskRepository by inject()
private val EXT_APK by lazy { File("${activity.filesDir.parent}/snet", "snet.jar") }
private val EXT_DEX by lazy { File(EXT_APK.parent, "snet.dex") }
override fun onResponse(responseCode: Int) = viewModel.finishSafetyNetCheck(responseCode)
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is OpenLinkEvent -> activity.openUrl(event.url)
is ManagerInstallEvent -> installManager()
is MagiskInstallEvent -> installMagisk()
is UninstallEvent -> uninstall()
is ManagerChangelogEvent -> changelogManager()
is EnvFixEvent -> fixEnv()
is UpdateSafetyNetEvent -> updateSafetyNet(false)
}
}
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onStart() {
super.onStart()
activity.title = resources.getString(R.string.section_home)
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.magisk)
}
private fun installMagisk() {
// Show Manager update first
if (Info.remote.app.versionCode > BuildConfig.VERSION_CODE) {
installManager()
return
}
MagiskInstallDialog(requireActivity() as BaseActivity<*, *>).show()
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_home_md2, menu)
}
private fun installManager() = ManagerInstallDialog(requireActivity()).show()
private fun uninstall() = UninstallDialog(requireActivity()).show()
private fun fixEnv() = EnvFixDialog(requireActivity()).show()
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_settings -> Navigation.settings().dispatchOnSelf()
else -> null
}?.let { true } ?: super.onOptionsItemSelected(item)
private fun changelogManager() = MarkDownWindow
.show(requireActivity(), null, resources.openRawResource(R.raw.changelog))
private fun downloadSafetyNet(requiresUserInput: Boolean = true) {
fun download() = magiskRepo.fetchSafetynet()
.map { it.byteStream().writeTo(EXT_APK) }
.subscribeK { updateSafetyNet(true) }
if (!requiresUserInput) {
download()
return
}
CustomAlertDialog(requireActivity())
.setTitle(R.string.proprietary_title)
.setMessage(R.string.proprietary_notice)
.setCancelable(false)
.setPositiveButton(android.R.string.yes) { _, _ -> download() }
.setNegativeButton(android.R.string.no) { _, _ -> viewModel.finishSafetyNetCheck(-2) }
.show()
}
private fun updateSafetyNet(dieOnError: Boolean) {
Completable.fromAction {
val loader = DynamicClassLoader(EXT_APK)
val dex = DexFile.loadDex(EXT_APK.path, EXT_DEX.path, 0)
// Scan through the dex and find our helper class
var helperClass: Class<*>? = null
for (className in dex.entries()) {
if (className.startsWith("x.")) {
val cls = loader.loadClass(className)
if (InvocationHandler::class.java.isAssignableFrom(cls)) {
helperClass = cls
break
}
}
}
helperClass ?: throw Exception()
val helper = helperClass.getMethod("get",
Class::class.java, Context::class.java, Any::class.java)
.invoke(null, SafetyNetHelper::class.java, activity, this) as SafetyNetHelper
if (helper.version < Const.SNET_EXT_VER)
throw Exception()
helper.attest()
}.subscribeK(onError = {
if (dieOnError) {
viewModel.finishSafetyNetCheck(-1)
} else {
Shell.sh("rm -rf " + EXT_APK.parent).exec()
EXT_APK.parentFile?.mkdir()
downloadSafetyNet(!dieOnError)
}
})
}
}
package com.topjohnwu.magisk.ui.home
import android.content.pm.PackageManager
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import android.Manifest
import android.os.Build
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.RemoteFileService
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.core.model.MagiskJson
import com.topjohnwu.magisk.core.model.ManagerJson
import com.topjohnwu.magisk.core.model.UpdateInfo
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Magisk
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Manager
import com.topjohnwu.magisk.model.entity.recycler.DeveloperItem
import com.topjohnwu.magisk.model.entity.recycler.HomeItem
import com.topjohnwu.magisk.model.events.OpenInappLinkEvent
import com.topjohnwu.magisk.model.events.dialog.EnvFixDialog
import com.topjohnwu.magisk.model.events.dialog.ManagerInstallDialog
import com.topjohnwu.magisk.model.events.dialog.UninstallDialog
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.model.observer.Observer
import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.SafetyNetHelper
import com.topjohnwu.superuser.Shell
import io.reactivex.Completable
enum class SafetyNetState {
LOADING, PASS, FAILED, IDLE
}
import me.tatarka.bindingcollectionadapter2.BR
import kotlin.math.roundToInt
enum class MagiskState {
NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING
}
enum class MagiskItem {
MANAGER, MAGISK
}
class HomeViewModel(
private val magiskRepo: MagiskRepository
) : BaseViewModel(State.LOADED) {
val hasGMS = runCatching {
get<PackageManager>().getPackageInfo("com.google.android.gms", 0); true
}.getOrElse { false }
val isAdvancedExpanded = KObservableField(false)
val isForceEncryption = KObservableField(Info.keepEnc)
val isKeepVerity = KObservableField(Info.keepVerity)
val isRecovery = KObservableField(Info.recovery)
val magiskState = KObservableField(MagiskState.LOADING)
val magiskStateText = Observer(magiskState) {
when (magiskState.value) {
MagiskState.NOT_INSTALLED -> R.string.magisk_version_error.res()
MagiskState.UP_TO_DATE -> R.string.magisk_up_to_date.res()
MagiskState.LOADING -> R.string.checking_for_updates.res()
MagiskState.OBSOLETE -> R.string.magisk_update_title.res()
private val repoMagisk: MagiskRepository
) : BaseViewModel() {
val isNoticeVisible = KObservableField(Config.safetyNotice)
val stateMagisk = KObservableField(MagiskState.LOADING)
val stateManager = KObservableField(MagiskState.LOADING)
val stateTextMagisk = Observer(stateMagisk) {
when (stateMagisk.value) {
MagiskState.NOT_INSTALLED -> R.string.installed_error_md2.res()
MagiskState.UP_TO_DATE -> R.string.up_to_date_md2.res()
MagiskState.LOADING -> R.string.loading_md2.res()
MagiskState.OBSOLETE -> R.string.obsolete_md2.res()
}
}
val magiskCurrentVersion = KObservableField("")
val magiskLatestVersion = KObservableField("")
val magiskAdditionalInfo = Observer(magiskState) {
if (Config.coreOnly)
R.string.core_only_enabled.res()
else
""
}
private val _managerState = KObservableField(MagiskState.LOADING)
val managerState = Observer(_managerState, isConnected) {
if (isConnected.value) _managerState.value else MagiskState.UP_TO_DATE
}
val managerStateText = Observer(managerState) {
when (managerState.value) {
MagiskState.NOT_INSTALLED -> R.string.invalid_update_channel.res()
MagiskState.UP_TO_DATE -> R.string.manager_up_to_date.res()
MagiskState.LOADING -> R.string.checking_for_updates.res()
MagiskState.OBSOLETE -> R.string.manager_update_title.res()
val stateTextManager = Observer(stateManager) {
when (stateManager.value) {
MagiskState.NOT_INSTALLED -> R.string.channel_error_md2.res()
MagiskState.UP_TO_DATE -> R.string.up_to_date_md2.res()
MagiskState.LOADING -> R.string.loading_md2.res()
MagiskState.OBSOLETE -> R.string.obsolete_md2.res()
}
}
val managerCurrentVersion = KObservableField("")
val managerLatestVersion = KObservableField("")
val managerAdditionalInfo = Observer(managerState) {
if (packageName != BuildConfig.APPLICATION_ID)
"($packageName)"
else
""
}
val statePackageManager = packageName
val statePackageOriginal = statePackageManager == BuildConfig.APPLICATION_ID
val stateVersionUpdateMagisk = KObservableField("")
val stateVersionUpdateManager = KObservableField("")
val safetyNetTitle = KObservableField(R.string.safetyNet_check_text.res())
val ctsState = KObservableField(SafetyNetState.IDLE)
val basicIntegrityState = KObservableField(SafetyNetState.IDLE)
val safetyNetState = Observer(ctsState, basicIntegrityState) {
val cts = ctsState.value
val basic = basicIntegrityState.value
val states = listOf(cts, basic)
val stateMagiskProgress = KObservableField(0)
val stateManagerProgress = KObservableField(0)
when {
states.any { it == SafetyNetState.LOADING } -> State.LOADING
states.any { it == SafetyNetState.IDLE } -> State.LOADING
else -> State.LOADED
}
}
val isActive = KObservableField(false)
private var shownDialog = false
val stateMagiskExpanded = KObservableField(false)
val stateManagerExpanded = KObservableField(false)
init {
isForceEncryption.addOnPropertyChangedCallback {
Info.keepEnc = it ?: return@addOnPropertyChangedCallback
}
isKeepVerity.addOnPropertyChangedCallback {
Info.keepVerity = it ?: return@addOnPropertyChangedCallback
}
isRecovery.addOnPropertyChangedCallback {
Info.recovery = it ?: return@addOnPropertyChangedCallback
}
isConnected.addOnPropertyChangedCallback {
if (it == true) refresh(false)
val stateHideManagerName = R.string.manager.res().let {
if (!statePackageOriginal) {
it.replaceRandomWithSpecial(3)
} else {
it
}
refresh(false)
}
fun paypalPressed() = OpenLinkEvent(Const.Url.PAYPAL_URL).publish()
fun patreonPressed() = OpenLinkEvent(Const.Url.PATREON_URL).publish()
fun twitterPressed() = OpenLinkEvent(Const.Url.TWITTER_URL).publish()
fun githubPressed() = OpenLinkEvent(Const.Url.SOURCE_CODE_URL).publish()
fun xdaPressed() = OpenLinkEvent(Const.Url.XDA_THREAD).publish()
fun uninstallPressed() = UninstallEvent().publish()
fun advancedPressed() = isAdvancedExpanded.toggle()
fun installPressed(item: MagiskItem) = when (item) {
MagiskItem.MANAGER -> ManagerInstallEvent().publish()
MagiskItem.MAGISK -> MagiskInstallEvent().publish()
val items = listOf(DeveloperItem.Mainline, DeveloperItem.App, DeveloperItem.Project)
val itemBinding = itemBindingOf<HomeItem> {
it.bindExtra(BR.viewModel, this)
}
fun cardPressed(item: MagiskItem) = when (item) {
MagiskItem.MANAGER -> ManagerChangelogEvent().publish()
MagiskItem.MAGISK -> MagiskChangelogEvent().publish()
val itemDeveloperBinding = itemBindingOf<DeveloperItem> {
it.bindExtra(BR.viewModel, this)
}
fun safetyNetPressed() {
ctsState.value = SafetyNetState.LOADING
basicIntegrityState.value = SafetyNetState.LOADING
safetyNetTitle.value = R.string.checking_safetyNet_status.res()
UpdateSafetyNetEvent().publish()
}
private var shownDialog = false
fun finishSafetyNetCheck(response: Int) = when {
response and 0x0F == 0 -> {
val hasCtsPassed = response and SafetyNetHelper.CTS_PASS != 0
val hasBasicIntegrityPassed = response and SafetyNetHelper.BASIC_PASS != 0
safetyNetTitle.value = R.string.safetyNet_check_success.res()
ctsState.value = if (hasCtsPassed) {
SafetyNetState.PASS
} else {
SafetyNetState.FAILED
}
basicIntegrityState.value = if (hasBasicIntegrityPassed) {
SafetyNetState.PASS
} else {
SafetyNetState.FAILED
}
}
response == -2 -> {
ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE
}
else -> {
ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE
safetyNetTitle.value = when (response) {
SafetyNetHelper.RESPONSE_ERR -> R.string.safetyNet_res_invalid.res()
else -> R.string.safetyNet_api_error.res()
init {
RemoteFileService.progressBroadcast.observeForever {
when (it?.second) {
is Magisk.Download,
is Magisk.Flash -> stateMagiskProgress.value = it.first.times(100f).roundToInt()
is Manager -> stateManagerProgress.value = it.first.times(100f).roundToInt()
}
}
}
@JvmOverloads
fun refresh(invalidate: Boolean = true) {
if (invalidate)
Info.envRef.invalidate()
override fun refresh() = repoMagisk.fetchUpdate()
.onErrorReturn { Info.remote }
.subscribeK { updateBy(it) }
isActive.value = Info.env.isActive
private fun updateBy(info: UpdateInfo) {
stateMagisk.value = when {
!info.magisk.isInstalled -> MagiskState.NOT_INSTALLED
info.magisk.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
val fetchUpdate = if (isConnected.value)
magiskRepo.fetchUpdate().ignoreElement()
else
Completable.complete()
stateManager.value = when {
!info.app.isUpdateChannelCorrect && isConnected.value -> MagiskState.NOT_INSTALLED
info.app.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
Completable.fromAction {
// Ensure value is ready
Info.env
}.andThen(fetchUpdate)
.applyViewModel(this)
.doOnSubscribeUi {
magiskState.value = MagiskState.LOADING
_managerState.value = MagiskState.LOADING
ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE
safetyNetTitle.value = R.string.safetyNet_check_text.res()
}.subscribeK {
updateSelf()
ensureEnv()
refreshVersions()
stateVersionUpdateMagisk.value = when {
info.magisk.isObsolete -> "%s > %s".format(
Info.env.magiskVersionString.clipVersion(info.magisk.version),
info.magisk.version.clipVersion(Info.env.magiskVersionString)
)
else -> ""
}
}
private fun refreshVersions() {
magiskCurrentVersion.value = if (magiskState.value != MagiskState.NOT_INSTALLED) {
VERSION_FMT.format(Info.env.magiskVersionString, Info.env.magiskVersionCode)
} else {
""
stateVersionUpdateManager.value = when {
info.app.isObsolete -> "%s > %s".format(
BuildConfig.VERSION_NAME.clipVersion(info.app.version),
info.app.version.clipVersion(BuildConfig.VERSION_NAME)
)
else -> ""
}
managerCurrentVersion.value = if (isRunningAsStub) MGR_VER_FMT
.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, Info.stub!!.version)
else
VERSION_FMT.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
ensureEnv()
}
private fun updateSelf() {
magiskState.value = when (Info.env.magiskVersionCode) {
in Int.MIN_VALUE .. 0 -> MagiskState.NOT_INSTALLED
in 1 until Info.remote.magisk.versionCode -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
fun onLinkPressed(link: String) = OpenInappLinkEvent(link).publish()
magiskLatestVersion.value =
VERSION_FMT.format(Info.remote.magisk.version, Info.remote.magisk.versionCode)
fun onDeletePressed() = UninstallDialog().publish()
_managerState.value = when (Info.remote.app.versionCode) {
in Int.MIN_VALUE .. 0 -> MagiskState.NOT_INSTALLED //wrong update channel
in (BuildConfig.VERSION_CODE + 1) .. Int.MAX_VALUE -> MagiskState.OBSOLETE
else -> {
if (Info.stub?.version ?: Int.MAX_VALUE < Info.remote.stub.versionCode)
MagiskState.OBSOLETE
else
MagiskState.UP_TO_DATE
}
}
fun onManagerPressed() = ManagerInstallDialog().publish()
managerLatestVersion.value = MGR_VER_FMT
.format(Info.remote.app.version, Info.remote.app.versionCode, Info.remote.stub.versionCode)
fun onMagiskPressed() = withPermissions(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
).map { check(it);it }
.subscribeK { Navigation.install().publish() }
.add()
fun toggle(kof: KObservableField<Boolean>) = kof.toggle()
fun hideNotice() {
Config.safetyNotice = false
isNoticeVisible.value = false
}
private fun ensureEnv() {
val invalidStates =
listOf(MagiskState.NOT_INSTALLED, MagiskState.LOADING)
val invalidStates = listOf(
MagiskState.NOT_INSTALLED,
MagiskState.LOADING
)
// Don't bother checking env when magisk is not installed, loading or already has been shown
if (invalidStates.any { it == magiskState.value } || shownDialog) return
if (!Shell.su("env_check").exec().isSuccess) {
shownDialog = true
EnvFixEvent().publish()
if (
invalidStates.any { it == stateMagisk.value } ||
shownDialog ||
// don't care for emulators either
Build.DEVICE.orEmpty().contains("generic") ||
Build.PRODUCT.orEmpty().contains("generic")
) {
return
}
Shell.su("env_check")
.toSingle()
.map { it.exec() }
.filter { !it.isSuccess }
.subscribeK {
shownDialog = true
EnvFixDialog().publish()
}
}
companion object {
private const val VERSION_FMT = "%s (%d)"
private const val MGR_VER_FMT = "%s (%d) (%d)"
private fun String.clipVersion(other: String = ""): String {
val thisVersion = substringBefore('-')
val otherVersion = other.substringBefore('-')
return if (thisVersion != otherVersion) thisVersion else substringAfter('-')
}
}
@Suppress("unused")
val MagiskJson.isInstalled
get() = Info.env.magiskVersionCode > 0
val MagiskJson.isObsolete
get() = Info.env.magiskVersionCode < versionCode && isInstalled
val ManagerJson.isUpdateChannelCorrect
get() = versionCode > 0
val ManagerJson.isObsolete
get() = BuildConfig.VERSION_CODE < versionCode
package com.topjohnwu.magisk.ui.install
import android.content.Intent
import androidx.core.graphics.Insets
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import com.topjohnwu.magisk.model.events.RequestFileEvent
import com.topjohnwu.magisk.ui.base.CompatFragment
import org.koin.androidx.viewmodel.ext.android.viewModel
class InstallFragment : CompatFragment<InstallViewModel, FragmentInstallMd2Binding>() {
override val layoutRes = R.layout.fragment_install_md2
override val viewModel by viewModel<InstallViewModel>()
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
viewModel.data.value = RequestFileEvent.resolve(requestCode, resultCode, data)
}
override fun onStart() {
super.onStart()
requireActivity().setTitle(R.string.install)
}
}
package com.topjohnwu.magisk.ui.install
import android.net.Uri
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.RemoteFileService
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.events.RequestFileEvent
import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import org.koin.core.get
import kotlin.math.roundToInt
class InstallViewModel : BaseViewModel(State.LOADED) {
val isRooted = Shell.rootAccess()
val isAB = isABDevice()
val step = KObservableField(0)
val method = KObservableField(-1)
val progress = KObservableField(0)
var data = KObservableField<Uri?>(null)
init {
RemoteFileService.reset()
RemoteFileService.progressBroadcast.observeForever {
val (progress, subject) = it ?: return@observeForever
if (subject !is DownloadSubject.Magisk) {
return@observeForever
}
this.progress.value = progress.times(100).roundToInt()
if (this.progress.value >= 100) {
// this might cause issues if the flash activity launches on top of this sooner
back()
}
}
method.addOnPropertyChangedCallback {
if (method.value == R.id.method_patch) {
RequestFileEvent().publish()
}
}
}
fun step(nextStep: Int) {
step.value = nextStep
}
fun install() = DownloadService(get()) {
subject = DownloadSubject.Magisk(resolveConfiguration())
}.also { state = State.LOADING }
// ---
private fun resolveConfiguration() = when (method.value) {
R.id.method_download -> Configuration.Download
R.id.method_patch -> Configuration.Patch(data.value!!)
R.id.method_direct -> Configuration.Flash.Primary
R.id.method_inactive_slot -> Configuration.Flash.Secondary
else -> throw IllegalArgumentException("Unknown value")
}
private fun isABDevice() = ShellUtils
.fastCmd("grep_prop ro.build.ab_update")
.let { it.isNotEmpty() && it.toBoolean() }
}
package com.topjohnwu.magisk.ui.log
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentLogBinding
import com.topjohnwu.magisk.model.events.PageChangedEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.ui.base.CompatFragment
import com.topjohnwu.magisk.utils.MotionRevealHelper
import org.koin.androidx.viewmodel.ext.android.viewModel
class LogFragment : BaseFragment<LogViewModel, FragmentLogBinding>() {
class LogFragment : CompatFragment<LogViewModel, FragmentLogMd2Binding>() {
override val layoutRes: Int = R.layout.fragment_log
override val viewModel: LogViewModel by viewModel()
override val layoutRes = R.layout.fragment_log_md2
override val viewModel by viewModel<LogViewModel>()
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is PageChangedEvent -> activity.invalidateOptionsMenu()
private var actionSave: MenuItem? = null
private var isMagiskLogVisible
get() = binding.logFilter.isVisible
set(value) {
MotionRevealHelper.withViews(binding.logFilter, binding.logFilterToggle, value)
actionSave?.isVisible = value
(activity as MainActivity).invalidateToolbar()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.logTabs.setupWithViewPager(binding.logContainer, true)
}
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
activity.setTitle(R.string.log)
activity.title = resources.getString(R.string.section_log)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.logFilterToggle.setOnClickListener {
isMagiskLogVisible = true
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_log, menu)
menu.findItem(R.id.menu_save).isVisible = viewModel.currentPage.value == 1
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_log_md2, menu)
actionSave = menu.findItem(R.id.action_save)?.also {
it.isVisible = isMagiskLogVisible
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_save -> activity.withExternalRW {
onSuccess {
viewModel.saveLog()
}
}
R.id.menu_clear -> viewModel.clearLog()
R.id.menu_refresh -> viewModel.refresh()
R.id.action_save -> viewModel.saveMagiskLog()
R.id.action_clear ->
if (isMagiskLogVisible) viewModel.clearMagiskLog()
else viewModel.clearLog()
}
return super.onOptionsItemSelected(item)
}
override fun onPreBind(binding: FragmentLogMd2Binding) = Unit
override fun onBackPressed(): Boolean {
if (binding.logFilter.isVisible) {
isMagiskLogVisible = false
return true
}
return true
return super.onBackPressed()
}
}
package com.topjohnwu.magisk.ui.log
import android.content.res.Resources
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.binding.BindingAdapter
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
import com.topjohnwu.magisk.model.entity.recycler.LogItemRvItem
import com.topjohnwu.magisk.model.entity.recycler.LogRvItem
import com.topjohnwu.magisk.model.entity.recycler.MagiskLogRvItem
import com.topjohnwu.magisk.model.events.PageChangedEvent
import com.topjohnwu.magisk.model.entity.recycler.ConsoleItem
import com.topjohnwu.magisk.model.entity.recycler.LogItem
import com.topjohnwu.magisk.model.entity.recycler.TextItem
import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.diffListOf
import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.superuser.Shell
import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter
import me.tatarka.bindingcollectionadapter2.OnItemBind
import io.reactivex.Completable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import java.io.File
import java.util.*
class LogViewModel(
private val resources: Resources,
private val logRepo: LogRepository
) : BaseViewModel(), BindingViewPagerAdapter.PageTitles<ComparableRvItem<*>> {
val itemsAdapter = BindingAdapter()
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@LogViewModel)
}
val currentPage = KObservableField(0)
private val currentItem get() = items[currentPage.value]
private val logItem get() = items[0] as LogRvItem
private val magiskLogItem get() = items[1] as MagiskLogRvItem
val scrollPosition = KObservableField(0)
private val repo: LogRepository
) : BaseViewModel() {
init {
currentPage.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
PageChangedEvent().publish()
}
// --- empty view
items.addAll(listOf(LogRvItem(), MagiskLogRvItem()))
refresh()
}
val itemEmpty = TextItem(R.string.log_data_none)
val itemMagiskEmpty = TextItem(R.string.log_data_magisk_none)
override fun getPageTitle(position: Int, item: ComparableRvItem<*>?) = when (item) {
is LogRvItem -> resources.getString(R.string.superuser)
is MagiskLogRvItem -> resources.getString(R.string.magisk)
else -> ""
}
// --- main view
fun scrollDownPressed() {
scrollPosition.value = magiskLogItem.items.size - 1
val items = diffListOf<LogItem>()
val itemBinding = itemBindingOf<LogItem> {
it.bindExtra(BR.viewModel, this)
}
fun refresh() {
fetchLogs().subscribeK { logItem.update(it) }
fetchMagiskLog().subscribeK { magiskLogItem.update(it) }
// --- console
val consoleAdapter = BindingAdapter()
val itemsConsole = diffListOf<ComparableRvItem<*>>()
val itemConsoleBinding = itemBindingOf<ComparableRvItem<*>> {}
override fun refresh(): Disposable {
val logs = repo.fetchLogs()
.map { it.map { LogItem(it) } }
.observeOn(Schedulers.computation())
.map { it to items.calculateDiff(it) }
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess {
items.firstOrNull()?.isTop = false
items.lastOrNull()?.isBottom = false
items.update(it.first, it.second)
items.firstOrNull()?.isTop = true
items.lastOrNull()?.isBottom = true
}
.ignoreElement()
val console = repo.fetchMagiskLogs()
.map { ConsoleItem(it) }
.toList()
.observeOn(Schedulers.computation())
.map { it to itemsConsole.calculateDiff(it) }
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess { itemsConsole.update(it.first, it.second) }
.ignoreElement()
return Completable.merge(listOf(logs, console)).subscribeK()
}
fun saveLog() {
fun saveMagiskLog() {
val now = Calendar.getInstance()
val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format(
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
......@@ -92,29 +96,18 @@ class LogViewModel(
}
}
fun clearLog() = when (currentItem) {
is LogRvItem -> clearLogs { refresh() }
is MagiskLogRvItem -> clearMagiskLogs { refresh() }
else -> Unit
}
private fun clearLogs(callback: () -> Unit) = logRepo.clearLogs()
.doOnSubscribeUi(callback)
.subscribeK { SnackbarEvent(R.string.logs_cleared).publish() }
fun clearMagiskLog() = repo.clearMagiskLogs()
.subscribeK {
SnackbarEvent(R.string.logs_cleared).publish()
requestRefresh()
}
.add()
private fun clearMagiskLogs(callback: () -> Unit) = logRepo.clearMagiskLogs()
.doOnComplete(callback)
.subscribeK { SnackbarEvent(R.string.logs_cleared).publish() }
fun clearLog() = repo.clearLogs()
.subscribeK {
SnackbarEvent(R.string.logs_cleared).publish()
requestRefresh()
}
.add()
private fun fetchLogs() = logRepo.fetchLogs()
.flattenAsFlowable { it }
.map { LogItemRvItem(it) }
.toList()
private fun fetchMagiskLog() = logRepo.fetchMagiskLogs()
.map { ConsoleRvItem(it) }
.toList()
}
package com.topjohnwu.magisk.ui.module
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
import com.topjohnwu.magisk.model.events.InstallExternalModuleEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.ui.ReselectionTarget
import com.topjohnwu.magisk.ui.base.CompatFragment
import com.topjohnwu.magisk.ui.base.hideKeyboard
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
import com.topjohnwu.magisk.utils.MotionRevealHelper
import com.topjohnwu.magisk.utils.PinchZoomTouchListener
import org.koin.androidx.viewmodel.ext.android.viewModel
class ModuleFragment : CompatFragment<ModuleViewModel, FragmentModuleMd2Binding>(),
ReselectionTarget {
override val layoutRes = R.layout.fragment_module_md2
override val viewModel by viewModel<ModuleViewModel>()
private val listeners = hashSetOf<EndlessRecyclerScrollListener>()
private var isFilterVisible
get() = binding.moduleFilter.isVisible
set(value) {
if (!value) hideKeyboard()
(activity as? MainActivity)?.requestNavigationHidden(value)
MotionRevealHelper.withViews(binding.moduleFilter, binding.moduleFilterToggle, value)
}
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
InstallExternalModuleEvent.onActivityResult(requireContext(), requestCode, resultCode, data)
}
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
activity.title = resources.getString(R.string.section_modules)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setEndlessScroller()
setEndlessSearch()
binding.moduleFilterToggle.setOnClickListener {
isFilterVisible = true
}
binding.moduleFilterInclude.moduleFilterDone.setOnClickListener {
isFilterVisible = false
}
binding.moduleFilterInclude.moduleFilterList.addOnScrollListener(object :
RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState != RecyclerView.SCROLL_STATE_IDLE) hideKeyboard()
}
})
PinchZoomTouchListener.attachTo(binding.moduleFilterInclude.moduleFilterList)
PinchZoomTouchListener.attachTo(binding.moduleList)
}
override fun onDestroyView() {
listeners.forEach {
binding.moduleList.removeOnScrollListener(it)
binding.moduleFilterInclude.moduleFilterList.removeOnScrollListener(it)
}
PinchZoomTouchListener.clear(binding.moduleList)
PinchZoomTouchListener.clear(binding.moduleFilterInclude.moduleFilterList)
super.onDestroyView()
}
override fun onBackPressed(): Boolean {
if (isFilterVisible) {
isFilterVisible = false
return true
}
return super.onBackPressed()
}
// ---
override fun onEventDispatched(event: ViewEvent) = when (event) {
is EndlessRecyclerScrollListener.ResetState -> listeners.forEach { it.resetState() }
else -> super.onEventDispatched(event)
}
// ---
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_module_md2, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_refresh -> viewModel.forceRefresh()
}
return super.onOptionsItemSelected(item)
}
// ---
override fun onReselected() {
binding.moduleList
.takeIf {
(it.layoutManager as? StaggeredGridLayoutManager)?.let {
it.findFirstVisibleItemPositions(IntArray(it.spanCount)).min()
} ?: 0 > 10
}
?.also { it.scrollToPosition(10) }
.let { binding.moduleList }
.also { it.post { it.smoothScrollToPosition(0) } }
}
// ---
override fun onPreBind(binding: FragmentModuleMd2Binding) = Unit
private fun setEndlessScroller() {
val lama = binding.moduleList.layoutManager ?: return
lama.isAutoMeasureEnabled = false
val listener = EndlessRecyclerScrollListener(lama, viewModel::loadRemote)
binding.moduleList.addOnScrollListener(listener)
listeners.add(listener)
}
private fun setEndlessSearch() {
val lama = binding.moduleFilterInclude.moduleFilterList.layoutManager ?: return
lama.isAutoMeasureEnabled = false
val listener = EndlessRecyclerScrollListener(lama, viewModel::loadMoreQuery)
binding.moduleFilterInclude.moduleFilterList.addOnScrollListener(listener)
listeners.add(listener)
}
}
package com.topjohnwu.magisk.ui.module
import android.content.res.Resources
import androidx.annotation.WorkerThread
import androidx.databinding.Bindable
import androidx.databinding.ObservableArrayList
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.download.RemoteFileService
import com.topjohnwu.magisk.core.model.module.Module
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.core.tasks.RepoUpdater
import com.topjohnwu.magisk.data.database.RepoByNameDao
import com.topjohnwu.magisk.data.database.RepoByUpdatedDao
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.model.entity.module.Module
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem
import com.topjohnwu.magisk.model.entity.recycler.SectionRvItem
import com.topjohnwu.magisk.model.events.InstallModuleEvent
import com.topjohnwu.magisk.extensions.reboot
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.recycler.*
import com.topjohnwu.magisk.model.events.InstallExternalModuleEvent
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
import com.topjohnwu.magisk.tasks.RepoUpdater
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.model.events.dialog.ModuleInstallDialog
import com.topjohnwu.magisk.ui.base.*
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
import com.topjohnwu.magisk.utils.KObservableField
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import me.tatarka.bindingcollectionadapter2.OnItemBind
import io.reactivex.schedulers.Schedulers
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
import timber.log.Timber
import kotlin.math.roundToInt
/*
* The repo fetching behavior should follow these rules:
*
* For the first time the repo list is queried in the app, it should ALWAYS fetch for
* updates. However, this particular fetch should go through RepoUpdater.invoke(false),
* which internally will set ETAGs when doing GET requests to GitHub's API and will
* only update repo DB only if the GitHub API shows that something is changed remotely.
*
* When a user explicitly requests a full DB refresh, it should ALWAYS do a full force
* refresh, which in code can be done with RepoUpdater.invoke(true). This will update
* every single repo's information regardless whether GitHub's API shows if there is
* anything changed or not.
* */
class ModuleViewModel(
private val resources: Resources,
private val repoUpdater: RepoUpdater,
private val repoDB: RepoDao
) : BaseViewModel() {
private val repoName: RepoByNameDao,
private val repoUpdated: RepoByUpdatedDao,
private val repoUpdater: RepoUpdater
) : BaseViewModel(), Queryable by Queryable.impl(1000) {
override val queryRunnable = Runnable { query() }
var query = ""
@Bindable get
set(value) {
if (field == value) return
field = value
notifyPropertyChanged(BR.query)
submitQuery()
// Yes we do lie about the search being loaded
searchLoading.value = true
}
private var queryJob: Disposable? = null
val searchLoading = KObservableField(false)
val itemsSearch = diffListOf<RepoItem>()
val itemSearchBinding = itemBindingOf<RepoItem> {
it.bindExtra(BR.viewModel, this)
}
private val itemNoneInstalled = TextItem(R.string.module_install_none)
private val itemNoneUpdatable = TextItem(R.string.module_update_none)
private val itemsInstalledHelpers = ObservableArrayList<TextItem>().also {
it.add(itemNoneInstalled)
}
private val itemsUpdatableHelpers = ObservableArrayList<TextItem>().also {
it.add(itemNoneUpdatable)
}
private val itemsCoreOnly = ObservableArrayList<SafeModeNotice>()
private val itemsInstalled = diffListOf<ModuleItem>()
private val itemsUpdatable = diffListOf<RepoItem.Update>()
private val itemsRemote = diffListOf<RepoItem.Remote>()
val adapter = adapterOf<ComparableRvItem<*>>()
val items = MergeObservableList<ComparableRvItem<*>>()
.insertList(itemsCoreOnly)
.insertItem(sectionActive)
.insertList(itemsInstalledHelpers)
.insertList(itemsInstalled)
.insertItem(InstallModule)
.insertItem(sectionUpdate)
.insertList(itemsUpdatableHelpers)
.insertList(itemsUpdatable)
.insertItem(sectionRemote)
.insertList(itemsRemote)!!
val itemBinding = itemBindingOf<ComparableRvItem<*>> {
it.bindExtra(BR.viewModel, this)
}
companion object {
private val sectionRemote = SectionTitle(
R.string.module_section_remote,
R.string.sorting_order
)
private val sectionUpdate = SectionTitle(
R.string.module_section_pending,
R.string.module_section_pending_action,
R.drawable.ic_update_md2
// enable with implementation of https://github.com/topjohnwu/Magisk/issues/2036
).also { it.hasButton = false }
val query = KObservableField("")
private val sectionActive = SectionTitle(
R.string.module_section_installed,
R.string.reboot,
R.drawable.ic_restart
).also { it.hasButton = false }
private val allItems = mutableListOf<ComparableRvItem<*>>()
init {
updateOrderIcon()
}
val itemsInstalled = DiffObservableList(ComparableRvItem.callback)
val itemsRemote = DiffObservableList(ComparableRvItem.callback)
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@ModuleViewModel)
private fun updateOrderIcon() {
sectionRemote.icon = when (Config.repoOrder) {
Config.Value.ORDER_NAME -> R.drawable.ic_order_name
Config.Value.ORDER_DATE -> R.drawable.ic_order_date
else -> return
}
}
}
private var queryDisposable: Disposable? = null
// ---
private var remoteJob: Disposable? = null
private var refetch = false
private val dao
get() = when (Config.repoOrder) {
Config.Value.ORDER_DATE -> repoUpdated
Config.Value.ORDER_NAME -> repoName
else -> throw IllegalArgumentException()
}
// ---
init {
query.addOnPropertyChangedCallback {
queryDisposable?.dispose()
queryDisposable = query()
RemoteFileService.reset()
RemoteFileService.progressBroadcast.observeForever {
val (progress, subject) = it ?: return@observeForever
if (subject !is DownloadSubject.Module) {
return@observeForever
}
update(subject.module, progress.times(100).roundToInt())
}
}
// ---
override fun refresh(): Disposable {
updateCoreOnlyWarning()
if (itemsRemote.isEmpty())
loadRemote()
return loadInstalled().subscribeK()
}
private fun loadInstalled() = Single.fromCallable { Module.loadModules() }
.map { it.map { ModuleItem(it) } }
.map { it.loadDetail() }
.map { it to itemsInstalled.calculateDiff(it) }
.applyViewModel(this)
.observeOn(AndroidSchedulers.mainThread())
.map {
itemsInstalled.update(it.first, it.second)
if (itemsInstalled.isNotEmpty())
itemsInstalledHelpers.remove(itemNoneInstalled)
it.first
}
.observeOn(Schedulers.io())
.map { loadUpdates(it) }
.map { it to itemsUpdatable.calculateDiff(it) }
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess {
itemsUpdatable.update(it.first, it.second)
if (itemsUpdatable.isNotEmpty())
itemsUpdatableHelpers.remove(itemNoneUpdatable)
}
refresh(false)
}
fun fabPressed() = OpenFilePickerEvent().publish()
fun repoPressed(item: RepoRvItem) = OpenChangelogEvent(item.item).publish()
fun downloadPressed(item: RepoRvItem) = InstallModuleEvent(item.item).publish()
fun refresh(force: Boolean) {
Single.fromCallable { Module.loadModules() }
.flattenAsFlowable { it }
.map { ModuleRvItem(it) }
.toList()
.map { it to itemsInstalled.calculateDiff(it) }
.doOnSuccessUi { itemsInstalled.update(it.first, it.second) }
.flatMap { repoUpdater(force) }
.flattenAsFlowable { repoDB.repos }
.map { RepoRvItem(it) }
.toList()
.doOnSuccess { allItems.update(it) }
.flatMap { queryRaw() }
.applyViewModel(this)
.subscribeK { itemsRemote.update(it.first, it.second) }
}
private fun query() = queryRaw()
.subscribeK { itemsRemote.update(it.first, it.second) }
private fun queryRaw(query: String = this.query.value) = allItems.toSingle()
.map { it.filterIsInstance<RepoRvItem>() }
.flattenAsFlowable { it }
.filter {
it.item.name.contains(query, ignoreCase = true) ||
it.item.author.contains(query, ignoreCase = true) ||
it.item.description.contains(query, ignoreCase = true)
.ignoreElement()!!
@Synchronized
fun loadRemote() {
// check for existing jobs
if (remoteJob?.isDisposed?.not() == true) {
return
}
if (itemsRemote.isEmpty()) {
EndlessRecyclerScrollListener.ResetState().publish()
}
.toList()
.map { if (query.isEmpty()) it.divide() else it }
.map { it to itemsRemote.calculateDiff(it) }
private fun List<RepoRvItem>.divide(): List<ComparableRvItem<*>> {
val installed = itemsInstalled.filterIsInstance<ModuleRvItem>()
fun loadRemoteDB(offset: Int) = Single
.fromCallable { dao.getRepos(offset) }
.map { it.map { RepoItem.Remote(it) } }
remoteJob = if (itemsRemote.isEmpty()) {
repoUpdater(refetch).andThen(loadRemoteDB(0))
} else {
loadRemoteDB(itemsRemote.size)
}.subscribeK(onError = Timber::e) {
itemsRemote.addAll(it)
}
refetch = false
}
fun forceRefresh() {
itemsRemote.clear()
itemsSearch.clear()
refetch = true
refresh()
submitQuery()
}
// ---
fun <T : ComparableRvItem<*>> List<T>.withTitle(text: Int) =
if (isEmpty()) this else listOf(SectionRvItem(resources.getString(text))) + this
override fun submitQuery() {
queryHandler.removeCallbacks(queryRunnable)
queryHandler.postDelayed(queryRunnable, queryDelay)
}
val groupedItems = groupBy { repo ->
installed.firstOrNull { it.item.id == repo.item.id }?.let {
if (it.item.versionCode < repo.item.versionCode) MODULE_UPDATABLE
else MODULE_INSTALLED
} ?: MODULE_REMOTE
private fun queryInternal(query: String, offset: Int): Single<List<RepoItem>> {
if (query.isBlank()) {
return Single.just(listOf<RepoItem>())
.doOnSubscribe { itemsSearch.clear() }
.subscribeOn(AndroidSchedulers.mainThread())
}
return Single.fromCallable { dao.searchRepos(query, offset) }
.map { it.map { RepoItem.Remote(it) } }
}
return groupedItems.getOrElse(MODULE_UPDATABLE) { listOf() }.withTitle(R.string.update_available) +
groupedItems.getOrElse(MODULE_INSTALLED) { listOf() }.withTitle(R.string.installed) +
groupedItems.getOrElse(MODULE_REMOTE) { listOf() }.withTitle(R.string.not_installed)
private fun query(query: String = this.query, offset: Int = 0) {
queryJob?.dispose()
queryJob = queryInternal(query, offset)
.map { it to itemsSearch.calculateDiff(it) }
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess { searchLoading.value = false }
.subscribeK { itemsSearch.update(it.first, it.second) }
}
companion object {
protected const val MODULE_INSTALLED = 0
protected const val MODULE_REMOTE = 1
protected const val MODULE_UPDATABLE = 2
@Synchronized
fun loadMoreQuery() {
if (queryJob?.isDisposed == false) return
queryJob = queryInternal(query, itemsSearch.size)
.subscribeK { itemsSearch.addAll(it) }
}
// ---
@WorkerThread
private fun List<ModuleItem>.loadDetail() = onEach { module ->
Single.fromCallable { dao.getRepoById(module.item.id)!! }
.subscribeK { module.repo = it }
.add()
}
private fun update(repo: Repo, progress: Int) =
Single.fromCallable { itemsRemote + itemsSearch }
.map { it.first { it.item.id == repo.id } }
.subscribeK { it.progress.value = progress }
.add()
private fun updateCoreOnlyWarning() {
if (Config.coreOnly) {
if (itemsCoreOnly.isNotEmpty()) return
itemsCoreOnly.add(SafeModeNotice)
} else {
itemsCoreOnly.clear()
}
}
// ---
@WorkerThread
private fun loadUpdates(installed: List<ModuleItem>) = installed
.mapNotNull { dao.getUpdatableRepoById(it.item.id, it.item.versionCode) }
.map { RepoItem.Update(it) }
// ---
fun updateActiveState() = Single.fromCallable { itemsInstalled.any { it.isModified } }
.subscribeK { sectionActive.hasButton = it }
.add()
fun sectionPressed(item: SectionTitle) = when (item) {
sectionActive -> reboot() //TODO add reboot picker, regular reboot is not always preferred
sectionRemote -> {
Config.repoOrder = when (Config.repoOrder) {
Config.Value.ORDER_NAME -> Config.Value.ORDER_DATE
Config.Value.ORDER_DATE -> Config.Value.ORDER_NAME
else -> Config.Value.ORDER_NAME
}
updateOrderIcon()
Single.fromCallable { itemsRemote }
.subscribeK {
itemsRemote.removeAll(it)
remoteJob?.dispose()
loadRemote()
}.add()
}
else -> Unit
}
fun downloadPressed(item: RepoItem) = ModuleInstallDialog(item.item).publish()
fun installPressed() = InstallExternalModuleEvent().publish()
fun infoPressed(item: RepoItem) = OpenChangelogEvent(item.item).publish()
fun infoPressed(item: ModuleItem) {
OpenChangelogEvent(item.repo ?: return).publish()
}
}
\ No newline at end of file
}
package com.topjohnwu.magisk.ui.module
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentModulesBinding
import com.topjohnwu.magisk.extensions.reboot
import com.topjohnwu.magisk.intent
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class ModulesFragment : BaseFragment<ModuleViewModel, FragmentModulesBinding>() {
override val layoutRes: Int = R.layout.fragment_modules
override val viewModel: ModuleViewModel by sharedViewModel()
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) {
// Get the URI of the selected file
val intent = activity.intent<FlashActivity>()
intent.setData(data.data).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP)
startActivity(intent)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.modulesContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
binding.modulesRefreshLayout.isEnabled = recyclerView.getChildAt(0).top >= 0
}
})
}
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is OpenFilePickerEvent -> selectFile()
}
}
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.modules)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_reboot, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.reboot -> {
reboot()
return true
}
R.id.reboot_recovery -> {
Shell.su("/system/bin/reboot recovery").submit()
return true
}
R.id.reboot_bootloader -> {
reboot("bootloader")
return true
}
R.id.reboot_download -> {
reboot("download")
return true
}
R.id.reboot_edl -> {
reboot("edl")
return true
}
else -> return false
}
}
private fun selectFile() {
activity.withExternalRW {
onSuccess {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/zip"
startActivityForResult(intent, Const.ID.FETCH_ZIP)
}
}
}
}
package com.topjohnwu.magisk.ui.module
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.SearchView
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentReposBinding
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.module.Repo
import com.topjohnwu.magisk.model.events.InstallModuleEvent
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class ReposFragment : BaseFragment<ModuleViewModel, FragmentReposBinding>(),
SearchView.OnQueryTextListener {
override val layoutRes: Int = R.layout.fragment_repos
override val viewModel: ModuleViewModel by sharedViewModel()
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.downloads)
}
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is OpenChangelogEvent -> openChangelog(event.item)
is InstallModuleEvent -> installModule(event.item)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_repo, menu)
val query = viewModel.query.value
val searchItem = menu.findItem(R.id.repo_search)
val searchView = searchItem.actionView as? SearchView
searchView?.run {
setOnQueryTextListener(this@ReposFragment)
setQuery(query, false)
}
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView?.isIconified = false
} else {
searchItem.collapseActionView()
searchView?.isIconified = true
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.repo_sort) {
AlertDialog.Builder(activity)
.setTitle(R.string.sorting_order)
.setSingleChoiceItems(
R.array.sorting_orders,
Config.repoOrder
) { d, which ->
Config.repoOrder = which
viewModel.refresh(false)
d.dismiss()
}.show()
}
return true
}
override fun onQueryTextSubmit(p0: String?): Boolean {
viewModel.query.value = p0.orEmpty()
return false
}
override fun onQueryTextChange(p0: String?): Boolean {
viewModel.query.value = p0.orEmpty()
return false
}
private fun openChangelog(item: Repo) {
MarkDownWindow.show(requireActivity(), null, item.readme)
}
@SuppressLint("MissingPermission")
private fun installModule(item: Repo) {
val context = activity
fun download(install: Boolean) = context.withExternalRW {
onSuccess {
DownloadService(context) {
val config = if (install) Configuration.Flash.Primary else Configuration.Download
subject = DownloadSubject.Module(item, config)
}
}
}
CustomAlertDialog(context)
.setTitle(context.getString(R.string.repo_install_title, item.name))
.setMessage(context.getString(R.string.repo_install_msg, item.downloadFilename))
.setCancelable(true)
.setPositiveButton(R.string.install) { _, _ -> download(true) }
.setNeutralButton(R.string.download) { _, _ -> download(false) }
.show()
}
}
package com.topjohnwu.magisk.ui.request
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ActivityRequestMd2Binding
import com.topjohnwu.magisk.ui.base.CompatActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
class RequestActivity : CompatActivity<RequestViewModel, ActivityRequestMd2Binding>() {
override val navHost = TODO()
override val layoutRes = R.layout.activity_request_md2
override val viewModel by viewModel<RequestViewModel>()
}
package com.topjohnwu.magisk.ui.request
import com.topjohnwu.magisk.ui.base.BaseViewModel
class RequestViewModel : BaseViewModel()
package com.topjohnwu.magisk.ui.safetynet
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentSafetynetMd2Binding
import com.topjohnwu.magisk.ui.base.CompatFragment
import org.koin.androidx.viewmodel.ext.android.viewModel
class SafetynetFragment : CompatFragment<SafetynetViewModel, FragmentSafetynetMd2Binding>() {
override val layoutRes = R.layout.fragment_safetynet_md2
override val viewModel by viewModel<SafetynetViewModel>()
override fun onStart() {
super.onStart()
activity.setTitle(R.string.safetyNet)
}
}
package com.topjohnwu.magisk.ui.safetynet
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.events.SafetyNetResult
import com.topjohnwu.magisk.model.events.UpdateSafetyNetEvent
import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.safetynet.SafetyNetState.*
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
import com.topjohnwu.magisk.core.utils.SafetyNetHelper
enum class SafetyNetState {
LOADING, PASS, FAILED, IDLE
}
class SafetynetViewModel(
rxBus: RxBus
) : BaseViewModel() {
private var currentState = IDLE
set(value) {
field = value
notifyStateChanged()
}
val safetyNetTitle = KObservableField(R.string.empty)
val ctsState = KObservableField(false)
val basicIntegrityState = KObservableField(false)
val isChecking @Bindable get() = currentState == LOADING
val isFailed @Bindable get() = currentState == FAILED
val isSuccess @Bindable get() = currentState == PASS
init {
rxBus.register<SafetyNetResult>()
.subscribeK { resolveResponse(it.responseCode) }
.add()
if (safetyNetResult >= 0) {
resolveResponse(safetyNetResult)
} else {
attest()
}
}
override fun notifyStateChanged() {
super.notifyStateChanged()
notifyPropertyChanged(BR.loading)
notifyPropertyChanged(BR.failed)
notifyPropertyChanged(BR.success)
}
private fun attest() {
currentState = LOADING
UpdateSafetyNetEvent().publish()
}
fun reset() = attest()
private fun resolveResponse(response: Int) = when {
response and 0x0F == 0 -> {
val hasCtsPassed = response and SafetyNetHelper.CTS_PASS != 0
val hasBasicIntegrityPassed = response and SafetyNetHelper.BASIC_PASS != 0
val result = hasCtsPassed && hasBasicIntegrityPassed
safetyNetResult = response
ctsState.value = hasCtsPassed
basicIntegrityState.value = hasBasicIntegrityPassed
currentState = if (result) PASS else FAILED
safetyNetTitle.value =
if (result) R.string.safetynet_attest_success
else R.string.safetynet_attest_failure
}
response == -2 -> {
currentState = FAILED
ctsState.value = false
basicIntegrityState.value = false
back()
}
else -> {
currentState = FAILED
ctsState.value = false
basicIntegrityState.value = false
safetyNetTitle.value = when (response) {
SafetyNetHelper.RESPONSE_ERR -> R.string.safetyNet_res_invalid
else -> R.string.safetyNet_api_error
}
}
}
companion object {
private var safetyNetResult = -1
}
}
package com.topjohnwu.magisk.ui.settings
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.LayoutInflater
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.databinding.DataBindingUtil
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.SwitchPreferenceCompat
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.base.BasePreferenceFragment
import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.databinding.CustomDownloadDialogBinding
import com.topjohnwu.magisk.databinding.DialogCustomNameBinding
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.observer.Observer
import com.topjohnwu.magisk.utils.*
import com.topjohnwu.superuser.Shell
import io.reactivex.Completable
import org.koin.android.ext.android.inject
import java.io.File
import android.view.View
import androidx.core.graphics.Insets
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentSettingsMd2Binding
import com.topjohnwu.magisk.ui.base.CompatFragment
import com.topjohnwu.magisk.utils.PinchZoomTouchListener
import org.koin.androidx.viewmodel.ext.android.viewModel
class SettingsFragment : BasePreferenceFragment() {
class SettingsFragment : CompatFragment<SettingsViewModel, FragmentSettingsMd2Binding>() {
private val repoDB: RepoDao by inject()
override val layoutRes = R.layout.fragment_settings_md2
override val viewModel by viewModel<SettingsViewModel>()
private lateinit var updateChannel: ListPreference
private lateinit var autoRes: ListPreference
private lateinit var suNotification: ListPreference
private lateinit var requestTimeout: ListPreference
private lateinit var rootConfig: ListPreference
private lateinit var multiuserConfig: ListPreference
private lateinit var nsConfig: ListPreference
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.settings)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.setStorageDeviceProtected()
setPreferencesFromResource(R.xml.app_settings, rootKey)
// Get preferences
updateChannel = findPreference(Config.Key.UPDATE_CHANNEL)!!
rootConfig = findPreference(Config.Key.ROOT_ACCESS)!!
autoRes = findPreference(Config.Key.SU_AUTO_RESPONSE)!!
requestTimeout = findPreference(Config.Key.SU_REQUEST_TIMEOUT)!!
suNotification = findPreference(Config.Key.SU_NOTIFICATION)!!
multiuserConfig = findPreference(Config.Key.SU_MULTIUSER_MODE)!!
nsConfig = findPreference(Config.Key.SU_MNT_NS)!!
val reauth = findPreference<SwitchPreferenceCompat>(Config.Key.SU_REAUTH)!!
val biometric = findPreference<SwitchPreferenceCompat>(Config.Key.SU_BIOMETRIC)!!
val generalCategory = findPreference<PreferenceCategory>("general")!!
val magiskCategory = findPreference<PreferenceCategory>("magisk")!!
val suCategory = findPreference<PreferenceCategory>("superuser")!!
val hideManager = findPreference<Preference>("hide")!!
val restoreManager = findPreference<Preference>("restore")!!
// Remove/Disable entries
// Only show canary channels if user is already on canary channel
// or the user have already chosen canary channel
if (!Utils.isCanary && Config.updateChannel < Config.Value.CANARY_CHANNEL) {
// Remove the last 2 entries
val entries = updateChannel.entries
updateChannel.entries = entries.copyOf(entries.size - 2)
}
// Remove dangerous settings in secondary user
if (Const.USER_ID > 0) {
suCategory.removePreference(multiuserConfig)
}
// Remove re-authentication option on Android O, it will not work
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
suCategory.removePreference(reauth)
}
// Disable biometric option if not possible
if (!BiometricHelper.isSupported) {
biometric.isEnabled = false
biometric.isChecked = false
biometric.setSummary(R.string.no_biometric)
}
if (Const.USER_ID == 0 && Info.isConnected.value && Info.env.isActive) {
if (activity.packageName == BuildConfig.APPLICATION_ID) {
generalCategory.removePreference(restoreManager)
hideManager.setOnPreferenceClickListener {
showManagerNameDialog {
PatchAPK.hideManager(requireContext(), it)
}
true
}
} else {
generalCategory.removePreference(hideManager)
restoreManager.setOnPreferenceClickListener {
DownloadService(requireContext()) {
subject = DownloadSubject.Manager(Configuration.APK.Restore)
}
true
}
}
} else {
// Remove if not primary user, no connection, or no root
generalCategory.removePreference(restoreManager)
generalCategory.removePreference(hideManager)
}
if (!Utils.showSuperUser()) {
preferenceScreen.removePreference(suCategory)
}
if (!Info.env.isActive) {
preferenceScreen.removePreference(magiskCategory)
generalCategory.removePreference(hideManager)
}
findPreference<Preference>("clear")?.also {
if (Info.env.isActive) {
it.setOnPreferenceClickListener {
Completable.fromAction { repoDB.clear() }.subscribeK {
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT)
}
true
}
} else {
generalCategory.removePreference(it)
}
}
findPreference<Preference>("hosts")?.setOnPreferenceClickListener {
Shell.su("add_hosts_module").submit {
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
}
true
}
findPreference<Preference>(Config.Key.DOWNLOAD_PATH)?.apply {
summary = Config.downloadPath
setOnPreferenceClickListener { pref ->
activity.withExternalRW {
onSuccess {
showDownloadDialog {
Config.downloadPath = it
pref.summary = it
}
}
}
true
}
}
updateChannel.setOnPreferenceChangeListener { _, value ->
val channel = value.toString().toInt()
val previous = Config.updateChannel
if (channel == Config.Value.CUSTOM_CHANNEL) {
showUrlDialog(Config.customChannelUrl, {
Config.updateChannel = previous
}, {
Config.customChannelUrl = it
})
}
true
}
setLocalePreference(findPreference(Config.Key.LOCALE)!!)
setSummary()
activity.title = resources.getString(R.string.section_settings)
}
override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String) {
fun getStrInt() = prefs.getString(key, null)?.toInt() ?: 0
when (key) {
Config.Key.ROOT_ACCESS -> Config.rootMode = getStrInt()
Config.Key.SU_MULTIUSER_MODE -> Config.suMultiuserMode = getStrInt()
Config.Key.SU_MNT_NS -> Config.suMntNamespaceMode = getStrInt()
Config.Key.DARK_THEME -> requireActivity().recreate()
Config.Key.COREONLY -> {
if (prefs.getBoolean(key, false)) {
runCatching {
Const.MAGISK_DISABLE_FILE.createNewFile()
}
} else {
Const.MAGISK_DISABLE_FILE.delete()
}
Utils.toast(R.string.settings_reboot_toast, Toast.LENGTH_LONG)
}
Config.Key.MAGISKHIDE -> if (prefs.getBoolean(key, false)) {
Shell.su("magiskhide --enable").submit()
} else {
Shell.su("magiskhide --disable").submit()
}
Config.Key.LOCALE -> {
refreshLocale()
activity.recreate()
}
Config.Key.CHECK_UPDATES -> Utils.scheduleUpdateCheck(activity)
}
setSummary(key)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
PinchZoomTouchListener.attachTo(binding.settingsList)
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
Config.Key.SU_BIOMETRIC -> {
val checked = (preference as SwitchPreferenceCompat).isChecked
preference.isChecked = !checked
BiometricHelper.authenticate(requireActivity()) {
preference.isChecked = checked
Config.suBiometric = checked
}
}
}
return true
override fun onDestroyView() {
PinchZoomTouchListener.clear(binding.settingsList)
super.onDestroyView()
}
private fun setLocalePreference(lp: ListPreference) {
lp.isEnabled = false
availableLocales.subscribeK { (names, values) ->
lp.isEnabled = true
lp.entries = names
lp.entryValues = values
lp.summary = currentLocale.getDisplayName(currentLocale)
}
override fun onResume() {
super.onResume()
viewModel.items.forEach { it.refresh() }
}
private fun setSummary(key: String) {
when (key) {
Config.Key.ROOT_ACCESS -> rootConfig.summary = resources
.getStringArray(R.array.su_access)[Config.rootMode]
Config.Key.SU_MULTIUSER_MODE -> multiuserConfig.summary = resources
.getStringArray(R.array.multiuser_summary)[Config.suMultiuserMode]
Config.Key.SU_MNT_NS -> nsConfig.summary = resources
.getStringArray(R.array.namespace_summary)[Config.suMntNamespaceMode]
Config.Key.UPDATE_CHANNEL -> {
var ch = Config.updateChannel
ch = if (ch < 0) Config.Value.STABLE_CHANNEL else ch
updateChannel.summary = resources
.getStringArray(R.array.update_channel)[ch]
}
Config.Key.SU_AUTO_RESPONSE -> autoRes.summary = resources
.getStringArray(R.array.auto_response)[Config.suAutoReponse]
Config.Key.SU_NOTIFICATION -> suNotification.summary = resources
.getStringArray(R.array.su_notification)[Config.suNotification]
Config.Key.SU_REQUEST_TIMEOUT -> requestTimeout.summary =
getString(R.string.request_timeout_summary, Config.suDefaultTimeout)
}
}
private fun setSummary() {
setSummary(Config.Key.ROOT_ACCESS)
setSummary(Config.Key.SU_MULTIUSER_MODE)
setSummary(Config.Key.SU_MNT_NS)
setSummary(Config.Key.UPDATE_CHANNEL)
setSummary(Config.Key.SU_AUTO_RESPONSE)
setSummary(Config.Key.SU_NOTIFICATION)
setSummary(Config.Key.SU_REQUEST_TIMEOUT)
}
private inline fun showUrlDialog(
initialValue: String,
crossinline onCancel: () -> Unit = {},
crossinline onSuccess: (String) -> Unit
) {
val v = LayoutInflater
.from(requireActivity())
.inflate(R.layout.custom_channel_dialog, null)
val url = v.findViewById<EditText>(R.id.custom_url).apply {
setText(initialValue)
}
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_update_custom)
.setView(v)
.setPositiveButton(android.R.string.ok) { _, _ -> onSuccess(url.text.toString()) }
.setNegativeButton(android.R.string.cancel) { _, _ -> onCancel() }
.setOnCancelListener { onCancel() }
.show()
}
inner class DownloadDialogData(initialValue: String) {
val text = KObservableField(initialValue)
val path = Observer(text) {
File(Environment.getExternalStorageDirectory(), text.value).absolutePath
}
}
private inline fun showDownloadDialog(
initialValue: String = Config.downloadPath,
crossinline onSuccess: (String) -> Unit
) {
val data = DownloadDialogData(initialValue)
val binding: CustomDownloadDialogBinding = DataBindingUtil
.inflate(layoutInflater, R.layout.custom_download_dialog, null, false)
binding.also { it.data = data }
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_download_path_title)
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ ->
Utils.ensureDownloadPath(data.text.value)?.let { onSuccess(data.text.value) }
?: Utils.toast(R.string.settings_download_path_error, Toast.LENGTH_SHORT)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private inline fun showManagerNameDialog(
crossinline onSuccess: (String) -> Unit
) {
val data = ManagerNameData()
val view = DialogCustomNameBinding
.inflate(LayoutInflater.from(requireContext()))
.also { it.data = data }
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_app_name)
.setView(view.root)
.setPositiveButton(android.R.string.ok) { _, _ ->
if (view.dialogNameInput.error.isNullOrBlank()) {
onSuccess(data.name.value)
}
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
inner class ManagerNameData {
val name = KObservableField(resources.getString(R.string.re_app_name))
}
}
package com.topjohnwu.magisk.ui.settings
import android.content.Context
import android.os.Build
import android.os.Environment
import android.view.LayoutInflater
import android.widget.Toast
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.utils.*
import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.recycler.SettingsItem
import com.topjohnwu.magisk.utils.asTransitive
import com.topjohnwu.superuser.Shell
import java.io.File
import kotlin.math.max
import kotlin.math.min
// --- Customization
object Customization : SettingsItem.Section() {
override val title = "Customization".asTransitive()
}
object Language : SettingsItem.Selector() {
override var value by bindableValue(0) {
Config.locale = entryValues[it]
refreshLocale()
}
override val title = R.string.language.asTransitive()
override var entries = arrayOf<String>()
override var entryValues = arrayOf<String>()
init {
availableLocales.subscribeK { (names, values) ->
entries = names
entryValues = values
val selectedLocale = currentLocale.getDisplayName(currentLocale)
value = names.indexOfFirst { it == selectedLocale }.let { if (it == -1) 0 else it }
notifyChange(BR.selectedEntry)
}
}
}
object Theme : SettingsItem.Blank() {
override val icon = R.drawable.ic_paint
override val title = R.string.section_theme.asTransitive()
}
// --- Manager
object Manager : SettingsItem.Section() {
override val title = R.string.manager.asTransitive()
}
object ClearRepoCache : SettingsItem.Blank() {
override val title = R.string.settings_clear_cache_title.asTransitive()
override val description = R.string.settings_clear_cache_summary.asTransitive()
override fun refresh() {
isEnabled = Info.env.isActive
}
}
object Hide : SettingsItem.Input() {
override val title = R.string.settings_hide_manager_title.asTransitive()
override val description = R.string.settings_hide_manager_summary.asTransitive()
override val showStrip = false
override var value: String = resources.getString(R.string.re_app_name)
set(value) {
field = value
notifyChange(BR.value)
notifyChange(BR.error)
}
@get:Bindable
val isError get() = value.length > 14 || value.isBlank()
override val intermediate: String?
get() = if (isError) null else value
override fun getView(context: Context) = DialogSettingsAppNameBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
}
object Restore : SettingsItem.Blank() {
override val title = R.string.settings_restore_manager_title.asTransitive()
override val description = R.string.settings_restore_manager_summary.asTransitive()
}
@Suppress("FunctionName")
fun HideOrRestore() =
if (get<Context>().packageName == BuildConfig.APPLICATION_ID) Hide else Restore
object DownloadPath : SettingsItem.Input() {
override var value: String by bindableValue(Config.downloadPath) { Config.downloadPath = it }
override val title = R.string.settings_download_path_title.asTransitive()
override val intermediate: String?
get() = if (Utils.ensureDownloadPath(result) != null) result else null
@get:Bindable
var result = value
set(value) {
field = value
notifyChange(BR.result)
notifyChange(BR.path)
}
@get:Bindable
val path
get() = File(Environment.getExternalStorageDirectory(), result).absolutePath.orEmpty()
override fun getView(context: Context) = DialogSettingsDownloadPathBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
}
object GridSize : SettingsItem.Selector() {
override var value by bindableValue(Config.listSpanCount - 1) {
Config.listSpanCount = max(1, min(3, it + 1))
}
override val title = R.string.settings_grid_span_count_title.asTransitive()
override val description = R.string.settings_grid_span_count_summary.asTransitive()
override val entries = resources.getStringArray(R.array.span_count)
override val entryValues = resources.getStringArray(R.array.value_array)
}
object UpdateChannel : SettingsItem.Selector() {
override var value by bindableValue(Config.updateChannel) { Config.updateChannel = it }
override val title = R.string.settings_update_channel_title.asTransitive()
override val entries = resources.getStringArray(R.array.update_channel).let {
if (!Utils.isCanary && Config.updateChannel < Config.Value.CANARY_CHANNEL)
it.take(it.size - 2).toTypedArray() else it
}
override val entryValues = resources.getStringArray(R.array.value_array)
}
object UpdateChannelUrl : SettingsItem.Input() {
override val title = R.string.settings_update_custom.asTransitive()
override var value by bindableValue(Config.customChannelUrl) { Config.customChannelUrl = it }
override val intermediate: String? get() = result
@get:Bindable
var result = value
set(value) {
field = value
notifyChange(BR.result)
}
override fun refresh() {
isEnabled = UpdateChannel.value == Config.Value.CUSTOM_CHANNEL
}
override fun getView(context: Context) = DialogSettingsUpdateChannelBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
}
object UpdateChecker : SettingsItem.Toggle() {
override val title = R.string.settings_check_update_title.asTransitive()
override val description = R.string.settings_check_update_summary.asTransitive()
override var value by bindableValue(Config.checkUpdate) {
Config.checkUpdate = it
Utils.scheduleUpdateCheck(get())
}
}
// check whether is module already installed beforehand?
object SystemlessHosts : SettingsItem.Blank() {
override val title = R.string.settings_hosts_title.asTransitive()
override val description = R.string.settings_hosts_summary.asTransitive()
}
object Biometrics : SettingsItem.Toggle() {
override val title = R.string.settings_su_biometric_title.asTransitive()
override var value by bindableValue(Config.suBiometric) { Config.suBiometric = it }
override var description = R.string.settings_su_biometric_summary.asTransitive()
override fun refresh() {
isEnabled = BiometricHelper.isSupported
if (!isEnabled) {
value = false
description = R.string.no_biometric.asTransitive()
}
}
}
object Reauthenticate : SettingsItem.Toggle() {
override val title = R.string.settings_su_reauth_title.asTransitive()
override val description = R.string.settings_su_reauth_summary.asTransitive()
override var value by bindableValue(Config.suReAuth) { Config.suReAuth = it }
override fun refresh() {
isEnabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.O && Utils.showSuperUser()
}
}
// --- Magisk
object Magisk : SettingsItem.Section() {
override val title = R.string.magisk.asTransitive()
}
object SafeMode : SettingsItem.Toggle() {
override val title = R.string.settings_safe_mode_title.asTransitive()
// Use old placeholder for now, will update text once native implementation is changed
override val description = R.string.settings_core_only_summary.asTransitive()
override var value by bindableValue(Config.coreOnly) {
if (Config.coreOnly == it) return@bindableValue
Config.coreOnly = it
when {
it -> runCatching { Const.MAGISK_DISABLE_FILE.createNewFile() }
else -> Const.MAGISK_DISABLE_FILE.delete()
}
Utils.toast(R.string.settings_reboot_toast, Toast.LENGTH_LONG)
}
}
object MagiskHide : SettingsItem.Toggle() {
override val title = R.string.magiskhide.asTransitive()
override val description = R.string.settings_magiskhide_summary.asTransitive()
override var value by bindableValue(Config.magiskHide) {
Config.magiskHide = it
when {
it -> Shell.su("magiskhide --enable").submit()
else -> Shell.su("magiskhide --disable").submit()
}
}
}
// --- Superuser
object Superuser : SettingsItem.Section() {
override val title = R.string.superuser.asTransitive()
}
object AccessMode : SettingsItem.Selector() {
override val title = R.string.superuser_access.asTransitive()
override val entries = resources.getStringArray(R.array.su_access)
override val entryValues = resources.getStringArray(R.array.value_array)
override var value by bindableValue(Config.rootMode) {
Config.rootMode = entryValues[it].toInt()
}
}
object MultiuserMode : SettingsItem.Selector() {
override val title = R.string.multiuser_mode.asTransitive()
override val entries = resources.getStringArray(R.array.multiuser_mode)
override val entryValues = resources.getStringArray(R.array.value_array)
private val descArray = resources.getStringArray(R.array.multiuser_summary)
override var value by bindableValue(Config.suMultiuserMode) {
Config.suMultiuserMode = entryValues[it].toInt()
}
override val description
get() = descArray[value].asTransitive()
override fun refresh() {
isEnabled = Const.USER_ID == 0
}
}
object MountNamespaceMode : SettingsItem.Selector() {
override val title = R.string.mount_namespace_mode.asTransitive()
override val entries = resources.getStringArray(R.array.namespace)
override val entryValues = resources.getStringArray(R.array.value_array)
private val descArray = resources.getStringArray(R.array.namespace_summary)
override var value by bindableValue(Config.suMntNamespaceMode) {
Config.suMntNamespaceMode = entryValues[it].toInt()
}
override val description
get() = descArray[value].asTransitive()
}
object AutomaticResponse : SettingsItem.Selector() {
override val title = R.string.auto_response.asTransitive()
override val entries = resources.getStringArray(R.array.auto_response)
override val entryValues = resources.getStringArray(R.array.value_array)
override var value by bindableValue(Config.suAutoReponse) {
Config.suAutoReponse = entryValues[it].toInt()
}
}
object RequestTimeout : SettingsItem.Selector() {
override val title = R.string.request_timeout.asTransitive()
override val entries = resources.getStringArray(R.array.request_timeout)
override val entryValues = resources.getStringArray(R.array.request_timeout_value)
override var value by bindableValue(selected) {
Config.suDefaultTimeout = entryValues[it].toInt()
}
private val selected: Int
get() = entryValues.indexOfFirst { it.toInt() == Config.suDefaultTimeout }
}
object SUNotification : SettingsItem.Selector() {
override val title = R.string.superuser_notification.asTransitive()
override val entries = resources.getStringArray(R.array.su_notification)
override val entryValues = resources.getStringArray(R.array.value_array)
override var value by bindableValue(Config.suNotification) {
Config.suNotification = entryValues[it].toInt()
}
}
package com.topjohnwu.magisk.ui.settings
import android.Manifest
import android.os.Build
import android.view.View
import android.widget.Toast
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.utils.PatchAPK
import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.recycler.SettingsItem
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.RecreateEvent
import com.topjohnwu.magisk.model.events.dialog.BiometricDialog
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.adapterOf
import com.topjohnwu.magisk.ui.base.diffListOf
import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.superuser.Shell
import io.reactivex.Completable
import io.reactivex.subjects.PublishSubject
import org.koin.core.get
class SettingsViewModel(
private val repositoryDao: RepoDao
) : BaseViewModel(), SettingsItem.Callback {
val adapter = adapterOf<SettingsItem>()
val itemBinding = itemBindingOf<SettingsItem> { it.bindExtra(BR.callback, this) }
val items = diffListOf(createItems())
private fun createItems(): List<SettingsItem> {
// Customization
val list = mutableListOf(
Customization,
Theme, Language, GridSize
)
if (Build.VERSION.SDK_INT < 21) {
// Pre 5.0 does not support getting colors from attributes,
// making theming a pain in the ass. Just forget about it
list.remove(Theme)
}
// Manager
list.addAll(listOf(
Manager,
UpdateChannel, UpdateChannelUrl, UpdateChecker, DownloadPath
))
if (Info.env.isActive) {
list.add(ClearRepoCache)
if (Const.USER_ID == 0 && Info.isConnected.value)
list.add(HideOrRestore())
}
// Magisk
if (Info.env.isActive) {
list.addAll(listOf(
Magisk,
MagiskHide, SystemlessHosts, SafeMode
))
}
// Superuser
if (Utils.showSuperUser()) {
list.addAll(listOf(
Superuser,
Biometrics, AccessMode, MultiuserMode, MountNamespaceMode,
AutomaticResponse, RequestTimeout, SUNotification
))
if (Build.VERSION.SDK_INT < 23) {
// Biometric is only available on 6.0+
list.remove(Biometrics)
}
if (Build.VERSION.SDK_INT < 26) {
// Re-authenticate is not feasible on 8.0+
list.add(Reauthenticate)
}
}
return list
}
override fun onItemPressed(view: View, item: SettingsItem) = when (item) {
is DownloadPath -> requireRWPermission()
else -> Unit
}
override fun onItemChanged(view: View, item: SettingsItem) = when (item) {
// use only instances you want, don't declare everything
is Theme -> Navigation.theme().publish()
is Language -> RecreateEvent().publish()
is UpdateChannel -> openUrlIfNecessary(view)
is Biometrics -> authenticateOrRevert()
is ClearRepoCache -> clearRepoCache()
is SystemlessHosts -> createHosts()
is Hide -> updateManager(hide = true)
is Restore -> updateManager(hide = false)
else -> Unit
}
private fun openUrlIfNecessary(view: View) {
UpdateChannelUrl.refresh()
if (UpdateChannelUrl.isEnabled && UpdateChannelUrl.value.isBlank()) {
UpdateChannelUrl.onPressed(view, this@SettingsViewModel)
}
}
private fun authenticateOrRevert() {
// immediately revert the preference
Biometrics.value = !Biometrics.value
BiometricDialog {
// allow the change on success
onSuccess { Biometrics.value = !Biometrics.value }
}.publish()
}
private fun clearRepoCache() {
Completable.fromAction { repositoryDao.clear() }
.subscribeK { Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT) }
}
private fun createHosts() {
Shell.su("add_hosts_module").submit {
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
}
}
private fun requireRWPermission() {
val callback = PublishSubject.create<Boolean>()
callback.subscribeK { if (!it) requireRWPermission() }
PermissionEvent(
listOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
), callback
).publish()
}
private fun updateManager(hide: Boolean) {
if (hide) {
PatchAPK.hideManager(get(), Hide.value)
} else {
DownloadService(get()) {
subject = DownloadSubject.Manager(Configuration.APK.Restore)
}
}
}
}
package com.topjohnwu.magisk.ui.superuser
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.graphics.Insets
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentSuperuserBinding
import com.topjohnwu.magisk.databinding.FragmentSuperuserMd2Binding
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.ui.base.CompatFragment
import com.topjohnwu.magisk.utils.PinchZoomTouchListener
import org.koin.androidx.viewmodel.ext.android.viewModel
class SuperuserFragment :
BaseFragment<SuperuserViewModel, FragmentSuperuserBinding>() {
class SuperuserFragment : CompatFragment<SuperuserViewModel, FragmentSuperuserMd2Binding>() {
override val layoutRes: Int = R.layout.fragment_superuser
override val viewModel: SuperuserViewModel by viewModel()
override val layoutRes = R.layout.fragment_superuser_md2
override val viewModel by viewModel<SuperuserViewModel>()
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onStart() {
super.onStart()
activity.title = resources.getString(R.string.section_superuser)
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.superuser)
}
override fun onResume() {
super.onResume()
viewModel.updatePolicies()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
PinchZoomTouchListener.attachTo(binding.superuserList)
}
override fun onDestroyView() {
PinchZoomTouchListener.clear(binding.superuserList)
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_superuser_md2, menu)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_log -> Navigation.log().dispatchOnSelf()
else -> null
}?.let { true } ?: super.onOptionsItemSelected(item)
}
......@@ -2,150 +2,162 @@ package com.topjohnwu.magisk.ui.superuser
import android.content.pm.PackageManager
import android.content.res.Resources
import androidx.databinding.ObservableArrayList
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.database.PolicyDao
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.model.MagiskPolicy
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.applySchedulers
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
import com.topjohnwu.magisk.model.entity.recycler.PolicyItem
import com.topjohnwu.magisk.model.entity.recycler.TappableHeadlineItem
import com.topjohnwu.magisk.model.entity.recycler.TextItem
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.utils.BiometricHelper
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.RxBus
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
import com.topjohnwu.magisk.model.events.dialog.BiometricDialog
import com.topjohnwu.magisk.model.events.dialog.SuperuserRevokeDialog
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.adapterOf
import com.topjohnwu.magisk.ui.base.diffListOf
import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.magisk.core.utils.BiometricHelper
import io.reactivex.Single
import io.reactivex.disposables.Disposable
import me.tatarka.bindingcollectionadapter2.ItemBinding
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
class SuperuserViewModel(
private val policyDB: PolicyDao,
private val db: PolicyDao,
private val packageManager: PackageManager,
private val resources: Resources,
rxBus: RxBus
) : BaseViewModel() {
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@SuperuserViewModel)
}
private val resources: Resources
) : BaseViewModel(), TappableHeadlineItem.Listener {
private var ignoreNext: PolicyRvItem? = null
private var fetchTask: Disposable? = null
private val itemNoData = TextItem(R.string.superuser_policy_none)
init {
rxBus.register<PolicyEnableEvent>()
.filter {
val isIgnored = it.item == ignoreNext
if (isIgnored) ignoreNext = null
!isIgnored
}
.subscribeK { togglePolicy(it.item, it.enable) }
.add()
rxBus.register<PolicyUpdateEvent>()
.subscribeK { updatePolicy(it) }
.add()
private val itemsPolicies = diffListOf<PolicyItem>()
private val itemsHelpers = ObservableArrayList<TextItem>().also {
it.add(itemNoData)
}
updatePolicies()
val adapter = adapterOf<ComparableRvItem<*>>()
val items = MergeObservableList<ComparableRvItem<*>>()
.insertItem(TappableHeadlineItem.Hide)
.insertItem(TappableHeadlineItem.Safetynet)
.insertList(itemsHelpers)
.insertList(itemsPolicies)
val itemBinding = itemBindingOf<ComparableRvItem<*>> {
it.bindExtra(BR.viewModel, this)
it.bindExtra(BR.listener, this)
}
fun updatePolicies() {
if (fetchTask?.isDisposed?.not() == true) return
fetchTask = policyDB.fetchAll()
.flattenAsFlowable { it }
.map { PolicyRvItem(it, it.applicationInfo.loadIcon(packageManager)) }
.toList()
.map {
it.sortedWith(compareBy(
{ it.item.appName.toLowerCase() },
{ it.item.packageName }
))
// ---
override fun refresh() = db.fetchAll()
.flattenAsFlowable { it }
.parallel()
.map { PolicyItem(it, it.applicationInfo.loadIcon(packageManager)) }
.sequential()
.sorted { o1, o2 ->
compareBy<PolicyItem>(
{ it.item.appName.toLowerCase(currentLocale) },
{ it.item.packageName }
).compare(o1, o2)
}
.toList()
.map { it to itemsPolicies.calculateDiff(it) }
.applySchedulers()
.applyViewModel(this)
.subscribeK {
itemsPolicies.update(it.first, it.second)
if (itemsPolicies.isNotEmpty()) {
itemsHelpers.remove(itemNoData)
}
.map { it to items.calculateDiff(it) }
.applySchedulers()
.applyViewModel(this)
.subscribeK { items.update(it.first, it.second) }
}
// ---
@Suppress("REDUNDANT_ELSE_IN_WHEN")
override fun onItemPressed(item: TappableHeadlineItem) = when (item) {
TappableHeadlineItem.Hide -> hidePressed()
TappableHeadlineItem.Safetynet -> safetynetPressed()
else -> Unit
}
fun deletePressed(item: PolicyRvItem) {
private fun safetynetPressed() = Navigation.safetynet().publish()
private fun hidePressed() = Navigation.hide().publish()
fun deletePressed(item: PolicyItem) {
fun updateState() = deletePolicy(item.item)
.map { items.filterIsInstance<PolicyRvItem>().toMutableList() }
.map { it.removeAll { it.item.packageName == item.item.packageName }; it }
.map { it to items.calculateDiff(it) }
.subscribeK { items.update(it.first, it.second) }
.subscribeK {
itemsPolicies.removeAll { it.genericItemSameAs(item) }
if (itemsPolicies.isEmpty() && itemsHelpers.isEmpty()) {
itemsHelpers.add(itemNoData)
}
}
.add()
withView {
if (BiometricHelper.isEnabled) {
BiometricHelper.authenticate(this) { updateState() }
} else {
CustomAlertDialog(this)
.setTitle(R.string.su_revoke_title)
.setMessage(getString(R.string.su_revoke_msg, item.item.appName))
.setPositiveButton(android.R.string.yes) { _, _ -> updateState() }
.setNegativeButton(android.R.string.no, null)
.setCancelable(true)
.show()
}
if (BiometricHelper.isEnabled) {
BiometricDialog {
onSuccess { updateState() }
}.publish()
} else {
SuperuserRevokeDialog {
appName = item.item.appName
onSuccess { updateState() }
}.publish()
}
}
private fun updatePolicy(it: PolicyUpdateEvent) = when (it) {
is PolicyUpdateEvent.Notification -> updatePolicy(it.item) {
val textId =
if (it.notification) R.string.su_snack_notif_on else R.string.su_snack_notif_off
val text = resources.getString(textId).format(it.appName)
SnackbarEvent(text).publish()
//---
fun updatePolicy(it: PolicyUpdateEvent) = when (it) {
is PolicyUpdateEvent.Notification -> updatePolicy(it.item).map {
when {
it.notification -> R.string.su_snack_notif_on
else -> R.string.su_snack_notif_off
} to it.appName
}
is PolicyUpdateEvent.Log -> updatePolicy(it.item) {
val textId =
if (it.logging) R.string.su_snack_log_on else R.string.su_snack_log_off
val text = resources.getString(textId).format(it.appName)
SnackbarEvent(text).publish()
is PolicyUpdateEvent.Log -> updatePolicy(it.item).map {
when {
it.logging -> R.string.su_snack_log_on
else -> R.string.su_snack_log_off
} to it.appName
}
}
}.map { resources.getString(it.first, it.second) }
.subscribeK { SnackbarEvent(it).publish() }
.add()
private fun updatePolicy(item: MagiskPolicy, onSuccess: (MagiskPolicy) -> Unit) =
updatePolicy(item)
.subscribeK { onSuccess(it) }
.add()
private fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
fun togglePolicy(item: PolicyItem, enable: Boolean) {
fun updateState() {
val app = item.item.copy(policy = if (enable) MagiskPolicy.ALLOW else MagiskPolicy.DENY)
val policy = if (enable) MagiskPolicy.ALLOW else MagiskPolicy.DENY
val app = item.item.copy(policy = policy)
updatePolicy(app)
.map { it.policy == MagiskPolicy.ALLOW }
.subscribeK {
val textId = if (it) R.string.su_snack_grant else R.string.su_snack_deny
val text = resources.getString(textId).format(item.item.appName)
SnackbarEvent(text).publish()
}
.map { if (it) R.string.su_snack_grant else R.string.su_snack_deny }
.map { resources.getString(it).format(item.item.appName) }
.subscribeK { SnackbarEvent(it).publish() }
.add()
}
if (BiometricHelper.isEnabled) {
withView {
BiometricHelper.authenticate(this, onError = {
ignoreNext = item
item.isEnabled.toggle()
}) { updateState() }
}
BiometricDialog {
onSuccess { updateState() }
onFailure { item.isEnabled.toggle() }
}.publish()
} else {
updateState()
}
}
//---
private fun updatePolicy(policy: MagiskPolicy) =
policyDB.update(policy).andThen(Single.just(policy))
db.update(policy).andThen(Single.just(policy))
private fun deletePolicy(policy: MagiskPolicy) =
policyDB.delete(policy.uid).andThen(Single.just(policy))
db.delete(policy.uid).andThen(Single.just(policy))
}
package com.topjohnwu.magisk.ui.surequest
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.CountDownTimer
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.database.PolicyDao
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.now
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem
import com.topjohnwu.magisk.model.entity.toPolicy
import com.topjohnwu.magisk.model.events.DieEvent
import com.topjohnwu.magisk.utils.BiometricHelper
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.SuConnector
import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter
import me.tatarka.bindingcollectionadapter2.ItemBinding
import timber.log.Timber
import java.io.IOException
import java.util.concurrent.TimeUnit.*
class SuRequestViewModel(
private val packageManager: PackageManager,
private val policyDB: PolicyDao,
private val timeoutPrefs: SharedPreferences,
private val resources: Resources
) : BaseViewModel() {
val icon = KObservableField<Drawable?>(null)
val title = KObservableField("")
val packageName = KObservableField("")
val denyText = KObservableField(resources.getString(R.string.deny))
val warningText = KObservableField<CharSequence>(resources.getString(R.string.su_warning))
val selectedItemPosition = KObservableField(0)
private val items = DiffObservableList(ComparableRvItem.callback)
private val itemBinding = ItemBinding.of<ComparableRvItem<*>> { binding, _, item ->
item.bind(binding)
}
val adapter = BindingListViewAdapter<ComparableRvItem<*>>(1).apply {
itemBinding = this@SuRequestViewModel.itemBinding
setItems(items)
}
private val cancelTasks = mutableListOf<() -> Unit>()
private lateinit var timer: CountDownTimer
private lateinit var policy: MagiskPolicy
private lateinit var connector: SuConnector
private fun cancelTimer() {
timer.cancel()
denyText.value = resources.getString(R.string.deny)
}
fun grantPressed() {
cancelTimer()
if (BiometricHelper.isEnabled) {
withView {
BiometricHelper.authenticate(this) {
handleAction(MagiskPolicy.ALLOW)
}
}
} else {
handleAction(MagiskPolicy.ALLOW)
}
}
fun denyPressed() {
handleAction(MagiskPolicy.DENY)
timer.cancel()
}
fun spinnerTouched(): Boolean {
cancelTimer()
return false
}
fun handleRequest(intent: Intent): Boolean {
val socketName = intent.getStringExtra("socket") ?: return false
try {
connector = Connector(socketName)
val map = connector.readRequest()
val uid = map["uid"]?.toIntOrNull() ?: return false
policy = uid.toPolicy(packageManager)
} catch (e: Exception) {
Timber.e(e)
return false
}
// Never allow com.topjohnwu.magisk (could be malware)
if (policy.packageName == BuildConfig.APPLICATION_ID)
return false
when (Config.suAutoReponse) {
Config.Value.SU_AUTO_DENY -> {
handleAction(MagiskPolicy.DENY, 0)
return true
}
Config.Value.SU_AUTO_ALLOW -> {
handleAction(MagiskPolicy.ALLOW, 0)
return true
}
}
showUI()
return true
}
private fun showUI() {
resources.getStringArray(R.array.allow_timeout)
.map { SpinnerRvItem(it) }
.let { items.update(it) }
icon.value = policy.applicationInfo.loadIcon(packageManager)
title.value = policy.appName
packageName.value = policy.packageName
selectedItemPosition.value = timeoutPrefs.getInt(policy.packageName, 0)
val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
timer = object : CountDownTimer(millis, 1000) {
override fun onTick(remains: Long) {
denyText.value = "${resources.getString(R.string.deny)} (${remains / 1000})"
}
override fun onFinish() {
denyText.value = resources.getString(R.string.deny)
handleAction(MagiskPolicy.DENY)
}
}
timer.start()
cancelTasks.add { cancelTimer() }
}
private fun handleAction() {
connector.response()
cancelTasks.forEach { it() }
DieEvent().publish()
}
private fun handleAction(action: Int) {
val pos = selectedItemPosition.value
timeoutPrefs.edit().putInt(policy.packageName, pos).apply()
handleAction(action, Config.Value.TIMEOUT_LIST[pos])
}
private fun handleAction(action: Int, time: Int) {
val until = if (time > 0)
MILLISECONDS.toSeconds(now) + MINUTES.toSeconds(time.toLong())
else
time.toLong()
policy.policy = action
policy.until = until
policy.uid = policy.uid % 100000 + Const.USER_ID * 100000
if (until >= 0)
policyDB.update(policy).blockingAwait()
handleAction()
}
private inner class Connector @Throws(Exception::class)
internal constructor(name: String) : SuConnector(name) {
@Throws(IOException::class)
override fun onResponse() {
out.writeInt(policy.policy)
}
}
}
package com.topjohnwu.magisk.ui.theme
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
enum class Theme(
val themeName: String,
val themeRes: Int
) {
Piplup(
themeName = "Piplup",
themeRes = R.style.ThemeFoundationMD2_Piplup
),
PiplupAmoled(
themeName = "AMOLED",
themeRes = R.style.ThemeFoundationMD2_Amoled
),
Rayquaza(
themeName = "Rayquaza",
themeRes = R.style.ThemeFoundationMD2_Rayquaza
),
Zapdos(
themeName = "Zapdos",
themeRes = R.style.ThemeFoundationMD2_Zapdos
),
Charmeleon(
themeName = "Charmeleon",
themeRes = R.style.ThemeFoundationMD2_Charmeleon
),
Mew(
themeName = "Mew",
themeRes = R.style.ThemeFoundationMD2_Mew
),
Salamence(
themeName = "Salamence",
themeRes = R.style.ThemeFoundationMD2_Salamence
);
val isSelected get() = Config.themeOrdinal == ordinal
fun select() {
Config.themeOrdinal = ordinal
}
companion object {
val selected get() = values().getOrNull(Config.themeOrdinal) ?: Piplup
}
}
package com.topjohnwu.magisk.ui.theme
import androidx.core.graphics.Insets
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentThemeMd2Binding
import com.topjohnwu.magisk.ui.base.CompatFragment
import org.koin.androidx.viewmodel.ext.android.viewModel
class ThemeFragment : CompatFragment<ThemeViewModel, FragmentThemeMd2Binding>() {
override val layoutRes = R.layout.fragment_theme_md2
override val viewModel by viewModel<ThemeViewModel>()
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onStart() {
super.onStart()
activity.title = getString(R.string.section_theme)
}
}
package com.topjohnwu.magisk.ui.theme
import com.topjohnwu.magisk.model.entity.recycler.TappableHeadlineItem
import com.topjohnwu.magisk.model.events.RecreateEvent
import com.topjohnwu.magisk.model.events.dialog.DarkThemeDialog
import com.topjohnwu.magisk.ui.base.BaseViewModel
class ThemeViewModel : BaseViewModel(), TappableHeadlineItem.Listener {
val themeHeadline = TappableHeadlineItem.ThemeMode
override fun onItemPressed(item: TappableHeadlineItem) = when (item) {
is TappableHeadlineItem.ThemeMode -> darkModePressed()
else -> Unit
}
fun saveTheme(theme: Theme) {
theme.select()
RecreateEvent().publish()
}
private fun darkModePressed() = DarkThemeDialog().publish()
}
package com.topjohnwu.magisk.utils
import android.animation.Animator
import android.animation.ValueAnimator
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.os.Build
import android.view.View
import android.view.ViewAnimationUtils
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextSwitcher
import android.widget.TextView
import android.widget.ViewSwitcher
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.Toolbar
import androidx.core.animation.doOnEnd
import androidx.core.view.*
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import androidx.drawerlayout.widget.DrawerLayout
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView
import com.google.android.material.textfield.TextInputLayout
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.extensions.drawableCompat
import com.topjohnwu.magisk.extensions.replaceRandomWithSpecial
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import io.reactivex.Observable
import io.reactivex.disposables.Disposable
import java.util.concurrent.TimeUnit
import kotlin.math.hypot
import kotlin.math.roundToInt
@BindingAdapter("onNavigationClick")
......@@ -228,4 +247,237 @@ fun TextInputLayout.setErrorString(error: String) {
val newError = error.let { if (it.isEmpty()) null else it }
if (this.error == null && newError == null) return
this.error = newError
}
// md2
@BindingAdapter("onSelectClick", "onSelectReset", requireAll = false)
fun View.setOnSelectClickListener(listener: View.OnClickListener, resetTime: Long) {
fun getHideTarget() = (parent as? ViewGroup)?.findViewWithTag<View>(R.id.hideWhenSelected)
fun animateVisibility(hide: Boolean, target: View? = getHideTarget()) {
target ?: return
val targetScale = if (hide) 0f else 1f
target.animate()
.scaleY(targetScale)
.scaleX(targetScale)
.start()
}
setOnClickListener {
when {
it.isSelected -> {
animateVisibility(false)
listener.onClick(it)
(it.tag as? Runnable)?.let { task ->
it.handler.removeCallbacks(task)
}
it.isSelected = false
}
else -> {
animateVisibility(true)
it.isSelected = true
it.tag = it.postDelayed(resetTime) {
animateVisibility(false)
it.tag = null
it.isSelected = false
}
}
}
}
}
@BindingAdapter("textCaptionVariant")
fun TextSwitcher.setTextBinding(text: CharSequence) {
tag as? ViewSwitcher.ViewFactory ?: ViewSwitcher.ViewFactory {
View.inflate(context, R.layout.swicher_caption_variant, null)
}.also {
tag = it
setFactory(it)
setInAnimation(context, R.anim.switcher_bottom_up)
setOutAnimation(context, R.anim.switcher_center_up)
}
val currentText = (currentView as? TextView)?.text
if (currentText != text) {
setText(text)
}
}
@BindingAdapter(
"android:layout_marginLeft",
"android:layout_marginTop",
"android:layout_marginRight",
"android:layout_marginBottom",
"android:layout_marginStart",
"android:layout_marginEnd",
requireAll = false
)
fun View.setMargins(
marginLeft: Int?,
marginTop: Int?,
marginRight: Int?,
marginBottom: Int?,
marginStart: Int?,
marginEnd: Int?
) = updateLayoutParams<ViewGroup.MarginLayoutParams> {
marginLeft?.let { leftMargin = it }
marginTop?.let { topMargin = it }
marginRight?.let { rightMargin = it }
marginBottom?.let { bottomMargin = it }
marginStart?.let { this.marginStart = it }
marginEnd?.let { this.marginEnd = it }
}
@BindingAdapter("nestedScrollingEnabled")
fun RecyclerView.setNestedScrolling(enabled: Boolean) {
isNestedScrollingEnabled = enabled
}
@BindingAdapter("isSelected")
fun View.isSelected(isSelected: Boolean) {
this.isSelected = isSelected
}
@BindingAdapter("reveal")
fun View.setRevealed(reveal: Boolean) {
val x = measuredWidth
val y = measuredHeight
val maxRadius = hypot(x.toDouble(), y.toDouble()).toFloat()
val start = if (reveal) 0f else maxRadius
val end = if (reveal) maxRadius else 0f
val anim = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
isInvisible = reveal
return
} else {
ViewAnimationUtils.createCircularReveal(this, x, 0, start, end).apply {
interpolator = FastOutSlowInInterpolator()
setTag(R.id.revealAnim, this)
doOnEnd { setTag(R.id.revealAnim, null) }
}
}
post {
isVisible = true
anim.start()
}
}
@BindingAdapter("revealFix")
fun View.setFixReveal(isRevealed: Boolean) {
(getTag(R.id.revealAnim) as? Animator)
?.doOnEnd { isInvisible = !isRevealed }
?.let { return }
isInvisible = !isRevealed
}
@BindingAdapter("dividerVertical", "dividerHorizontal", requireAll = false)
fun RecyclerView.setDividers(dividerVertical: Int, dividerHorizontal: Int) {
val horizontal = if (dividerHorizontal > 0) {
context.drawableCompat(dividerHorizontal)
} else {
null
}
val vertical = if (dividerVertical > 0) {
context.drawableCompat(dividerVertical)
} else {
null
}
setDividers(vertical, horizontal)
}
@BindingAdapter("dividerVertical", "dividerHorizontal", requireAll = false)
fun RecyclerView.setDividers(dividerVertical: Drawable?, dividerHorizontal: Drawable?) {
if (dividerHorizontal != null) {
DividerItemDecoration(context, LinearLayoutManager.HORIZONTAL).apply {
setDrawable(dividerHorizontal)
}.let { addItemDecoration(it) }
}
if (dividerVertical != null) {
DividerItemDecoration(context, LinearLayoutManager.VERTICAL).apply {
setDrawable(dividerVertical)
}.let { addItemDecoration(it) }
}
}
@BindingAdapter("rotationAnimated")
fun View.rotationTo(value: Int) {
animate()
.rotation(value.toFloat())
.setInterpolator(FastOutSlowInInterpolator())
.start()
}
@BindingAdapter("app:icon")
fun MaterialButton.setIconRes(res: Int) {
setIconResource(res)
}
@BindingAdapter("cardElevation")
fun MaterialCardView.setCardElevationBound(elevation: Float) {
cardElevation = elevation
}
@BindingAdapter("strokeWidth")
fun MaterialCardView.setCardStrokeWidthBound(stroke: Float) {
strokeWidth = stroke.roundToInt()
}
@BindingAdapter("onMenuClick")
fun Toolbar.setOnMenuClickListener(listener: Toolbar.OnMenuItemClickListener) {
setOnMenuItemClickListener(listener)
}
@BindingAdapter("tooltipText")
fun View.setTooltipTextCompat(text: String) {
ViewCompat.setTooltipText(this, text)
}
@BindingAdapter("onCloseClicked")
fun Chip.setOnCloseClickedListenerBinding(listener: View.OnClickListener) {
setOnCloseIconClickListener(listener)
}
@BindingAdapter("progressAnimated")
fun ProgressBar.setProgressAnimated(newProgress: Int) {
val animator = tag as? ValueAnimator
animator?.cancel()
ValueAnimator.ofInt(progress, newProgress).apply {
interpolator = FastOutSlowInInterpolator()
addUpdateListener { progress = it.animatedValue as Int }
tag = this
}.start()
}
@BindingAdapter("android:rotation")
fun View.setRotationNotAnimated(rotation: Int) {
if (animation != null) {
this.rotation = rotation.toFloat()
}
}
@BindingAdapter("android:text")
fun TextView.setTextSafe(text: Int) {
if (text == 0) this.text = null else setText(text)
}
@BindingAdapter("android:onLongClick")
fun View.setOnLongClickListenerBinding(listener: () -> Unit) {
setOnLongClickListener {
listener()
true
}
}
@BindingAdapter("strikeThrough")
fun TextView.setStrikeThroughEnabled(useStrikeThrough: Boolean) {
paintFlags = if (useStrikeThrough) {
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
}
\ No newline at end of file
......@@ -18,9 +18,9 @@ open class DiffObservableList<T>(
) : AbstractList<T>(), ObservableList<T> {
private val LIST_LOCK = Object()
private var list: MutableList<T> = ArrayList()
protected var list: MutableList<T> = ArrayList()
private val listeners = ListChangeRegistry()
private val listCallback = ObservableListUpdateCallback()
protected val listCallback = ObservableListUpdateCallback()
override val size: Int get() = list.size
......@@ -38,7 +38,7 @@ open class DiffObservableList<T>(
return doCalculateDiff(frozenList, newItems)
}
private fun doCalculateDiff(oldItems: List<T>, newItems: List<T>?): DiffUtil.DiffResult {
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>?): DiffUtil.DiffResult {
return DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize() = oldItems.size
......
package com.topjohnwu.magisk.utils
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.topjohnwu.magisk.model.events.ViewEvent
class EndlessRecyclerScrollListener(
private val layoutManager: RecyclerView.LayoutManager,
private val loadMore: (page: Int, totalItemsCount: Int, view: RecyclerView?) -> Unit,
private val direction: Direction = Direction.BOTTOM,
visibleRowsThreshold: Int = VISIBLE_THRESHOLD
) : RecyclerView.OnScrollListener() {
constructor(
layoutManager: RecyclerView.LayoutManager,
loadMore: () -> Unit,
direction: Direction = Direction.BOTTOM,
visibleRowsThreshold: Int = VISIBLE_THRESHOLD
) : this(layoutManager, { _, _, _ -> loadMore() }, direction, visibleRowsThreshold)
enum class Direction {
TOP, BOTTOM
}
companion object {
private const val VISIBLE_THRESHOLD = 5
private const val STARTING_PAGE_INDEX = 0
}
// The minimum amount of items to have above/below your current scroll position
// before loading more.
private val visibleThreshold = when (layoutManager) {
is LinearLayoutManager -> visibleRowsThreshold
is GridLayoutManager -> visibleRowsThreshold * layoutManager.spanCount
is StaggeredGridLayoutManager -> visibleRowsThreshold * layoutManager.spanCount
else -> throw IllegalArgumentException("Only LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager are supported")
}
// The current offset index of data you have loaded
private var currentPage = 0
// The total number of items in the dataset after the last load
private var previousTotalItemCount = 0
// True if we are still waiting for the last set of data to load.
private var loading = true
// This happens many times a second during a scroll, so be wary of the code you place here.
// We are given a few useful parameters to help us work out if we need to load some more data,
// but first we check if we are waiting for the previous load to finish.
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
if (dx == 0 && dy == 0) return
val totalItemCount = layoutManager.itemCount
val visibleItemPosition = if (direction == Direction.BOTTOM) {
when (layoutManager) {
is StaggeredGridLayoutManager -> layoutManager.findLastVisibleItemPositions(null).max()
?: 0
is GridLayoutManager -> layoutManager.findLastVisibleItemPosition()
is LinearLayoutManager -> layoutManager.findLastVisibleItemPosition()
else -> throw IllegalArgumentException("Only LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager are supported")
}
} else {
when (layoutManager) {
is StaggeredGridLayoutManager -> layoutManager.findFirstVisibleItemPositions(null).min()
?: 0
is GridLayoutManager -> layoutManager.findFirstVisibleItemPosition()
is LinearLayoutManager -> layoutManager.findFirstVisibleItemPosition()
else -> throw IllegalArgumentException("Only LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager are supported")
}
}
// If the total item count is zero and the previous isn't, assume the
// list is invalidated and should be reset back to initial state
if (totalItemCount < previousTotalItemCount) {
currentPage =
STARTING_PAGE_INDEX
previousTotalItemCount = totalItemCount
if (totalItemCount == 0) {
loading = true
}
}
// If it’s still loading, we check to see if the dataset count has
// changed, if so we conclude it has finished loading and update the current page
// number and total item count.
if (loading && totalItemCount > previousTotalItemCount) {
loading = false
previousTotalItemCount = totalItemCount
}
// If it isn’t currently loading, we check to see if we have breached
// the visibleThreshold and need to reload more data.
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
// threshold should reflect how many total columns there are too
if (!loading && shouldLoadMoreItems(visibleItemPosition, totalItemCount)) {
currentPage++
loadMore(currentPage, totalItemCount, view)
loading = true
}
}
private fun shouldLoadMoreItems(visibleItemPosition: Int, itemCount: Int) = when (direction) {
Direction.TOP -> visibleItemPosition < visibleThreshold
Direction.BOTTOM -> visibleItemPosition + visibleThreshold > itemCount
}
// Call this method whenever performing new searches
fun resetState() {
currentPage = STARTING_PAGE_INDEX
previousTotalItemCount = 0
loading = true
}
class ResetState : ViewEvent()
}
\ No newline at end of file
package com.topjohnwu.magisk.utils
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import java.util.*
class FilterableDiffObservableList<T>(
callback: Callback<T>
) : DiffObservableList<T>(callback) {
var filter: ((T) -> Boolean)? = null
set(value) {
field = value
queueUpdate()
}
@Volatile
private var sublist: MutableList<T> = super.list
// ---
private val ui by lazy { Handler(Looper.getMainLooper()) }
private val handler = Handler(HandlerThread("List${hashCode()}").apply { start() }.looper)
private val updater = Runnable {
val filter = filter ?: { true }
val newList = super.list.filter(filter)
val diff = synchronized(this) { doCalculateDiff(sublist, newList) }
ui.post {
sublist = Collections.synchronizedList(newList)
diff.dispatchUpdatesTo(listCallback)
}
}
private fun queueUpdate() {
handler.removeCallbacks(updater)
handler.post(updater)
}
fun hasFilter() = filter != null
fun filter(switch: (T) -> Boolean) {
filter = switch
}
fun reset() {
filter = null
}
// ---
override fun get(index: Int): T {
return sublist.get(index)
}
override fun add(element: T): Boolean {
return sublist.add(element)
}
override fun add(index: Int, element: T) {
sublist.add(index, element)
}
override fun addAll(elements: Collection<T>): Boolean {
return sublist.addAll(elements)
}
override fun addAll(index: Int, elements: Collection<T>): Boolean {
return sublist.addAll(index, elements)
}
override fun remove(element: T): Boolean {
return sublist.remove(element)
}
override fun removeAt(index: Int): T {
return sublist.removeAt(index)
}
override fun set(index: Int, element: T): T {
return sublist.set(index, element)
}
override val size: Int
get() = sublist.size
}
\ No newline at end of file
package com.topjohnwu.magisk.utils
import android.view.View
import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.R
import kotlin.math.roundToInt
class HideBottomViewOnScrollBehavior<V : View> : HideBottomViewOnScrollBehavior<V>(),
HideableBehavior<V> {
private var lockState: Boolean = false
override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View) =
super.layoutDependsOn(parent, child, dependency) or (dependency is Snackbar.SnackbarLayout)
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: V,
dependency: View
) = when (dependency) {
is Snackbar.SnackbarLayout -> onDependentViewChanged(parent, child, dependency)
else -> super.onDependentViewChanged(parent, child, dependency)
}
override fun onDependentViewRemoved(
parent: CoordinatorLayout,
child: V,
dependency: View
) = when (dependency) {
is Snackbar.SnackbarLayout -> onDependentViewRemoved(parent, child, dependency)
else -> super.onDependentViewRemoved(parent, child, dependency)
}
//---
private fun onDependentViewChanged(
parent: CoordinatorLayout,
child: V,
dependency: Snackbar.SnackbarLayout
): Boolean {
val viewMargin = (child.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin
val additionalMargin = dependency.resources.getDimension(R.dimen.l1).roundToInt()
val translation = dependency.height + additionalMargin
dependency.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = viewMargin
}
// checks whether the navigation is not hidden via scroll
if (child.isVisible && child.translationY <= 0) {
child.translationY(-translation.toFloat())
}
return false
}
private fun onDependentViewRemoved(
parent: CoordinatorLayout,
child: V,
dependency: Snackbar.SnackbarLayout
) {
// checks whether the navigation is not hidden via scroll
if (child.isVisible && child.translationY <= 0) {
child.translationY(0f)
}
}
//---
override fun slideUp(child: V) {
if (lockState) return
super.slideUp(child)
}
override fun slideDown(child: V) {
if (lockState) return
super.slideDown(child)
}
override fun setHidden(
view: V,
hide: Boolean,
lockState: Boolean
) {
if (!lockState) {
this.lockState = lockState
}
if (hide) {
slideDown(view)
} else {
slideUp(view)
}
this.lockState = lockState
}
//---
private fun View.translationY(destination: Float) = animate()
.translationY(destination)
.setInterpolator(FastOutSlowInInterpolator())
.start()
}
\ No newline at end of file
package com.topjohnwu.magisk.utils
import android.animation.TimeInterpolator
import android.view.View
import android.view.ViewGroup
import android.view.ViewPropertyAnimator
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import com.google.android.material.animation.AnimationUtils
class HideTopViewOnScrollBehavior<V : View> :
CoordinatorLayout.Behavior<V>(),
HideableBehavior<V> {
companion object {
private const val STATE_SCROLLED_DOWN = 1
private const val STATE_SCROLLED_UP = 2
private const val ENTER_ANIMATION_DURATION = 225
private const val EXIT_ANIMATION_DURATION = 175
}
private var height = 0
private var currentState = STATE_SCROLLED_UP
private var currentAnimator: ViewPropertyAnimator? = null
private var lockState: Boolean = false
override fun onLayoutChild(
parent: CoordinatorLayout,
child: V,
layoutDirection: Int
): Boolean {
val paramsCompat = child.layoutParams as ViewGroup.MarginLayoutParams
height = child.measuredHeight + paramsCompat.topMargin
return super.onLayoutChild(parent, child, layoutDirection)
}
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
directTargetChild: View,
target: View,
nestedScrollAxes: Int,
type: Int
) = nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL
override fun onNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
) {
// when initiating scroll while the view is at the bottom or at the top and pushing it
// further, the parent will report consumption of 0
if (dyConsumed == 0) return
setHidden(child, dyConsumed > 0, false)
}
@Suppress("UNCHECKED_CAST")
override fun setHidden(
view: V,
hide: Boolean,
lockState: Boolean
) {
if (!lockState) {
this.lockState = lockState
}
if (hide) {
slideUp(view)
} else {
slideDown(view)
}
this.lockState = lockState
}
/**
* Perform an animation that will slide the child from it's current position to be totally on the
* screen.
*/
private fun slideDown(child: V) {
if (currentState == STATE_SCROLLED_UP || lockState) {
return
}
currentAnimator?.let {
it.cancel()
child.clearAnimation()
}
currentState = STATE_SCROLLED_UP
animateChildTo(
child,
0,
ENTER_ANIMATION_DURATION.toLong(),
AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR
)
}
/**
* Perform an animation that will slide the child from it's current position to be totally off the
* screen.
*/
private fun slideUp(child: V) {
if (currentState == STATE_SCROLLED_DOWN || lockState) {
return
}
currentAnimator?.let {
it.cancel()
child.clearAnimation()
}
currentState = STATE_SCROLLED_DOWN
animateChildTo(
child,
-height,
EXIT_ANIMATION_DURATION.toLong(),
AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR
)
}
private fun animateChildTo(
child: V,
targetY: Int,
duration: Long,
interpolator: TimeInterpolator
) = child
.animate()
.translationY(targetY.toFloat())
.setInterpolator(interpolator)
.setDuration(duration)
.withEndAction { currentAnimator = null }
.let { currentAnimator = it }
}
\ No newline at end of file
package com.topjohnwu.magisk.utils
import android.view.View
interface HideableBehavior<V : View> {
fun setHidden(view: V, hide: Boolean, lockState: Boolean = false)
}
\ No newline at end of file
......@@ -10,6 +10,7 @@ import androidx.core.view.get
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.extensions.drawableCompat
import kotlin.math.roundToInt
class KItemDecoration(
private val context: Context,
......@@ -63,7 +64,7 @@ class KItemDecoration(
.map { parent[it] }
.forEach { child ->
parent.getDecoratedBoundsWithMargins(child, bounds)
val bottom = bounds.bottom + Math.round(child.translationY)
val bottom = bounds.bottom + child.translationY.roundToInt()
val top = bottom - drawable.intrinsicHeight
drawable.setBounds(left, top, right, bottom)
drawable.draw(canvas)
......@@ -93,7 +94,7 @@ class KItemDecoration(
.map { parent[it] }
.forEach { child ->
parent.layoutManager!!.getDecoratedBoundsWithMargins(child, bounds)
val right = bounds.right + Math.round(child.translationX)
val right = bounds.right + child.translationX.roundToInt()
val left = right - drawable.intrinsicWidth
drawable.setBounds(left, top, right, bottom)
drawable.draw(canvas)
......@@ -114,4 +115,4 @@ class KItemDecoration(
}
}
}
\ No newline at end of file
}
......@@ -2,18 +2,30 @@ package com.topjohnwu.magisk.utils
import androidx.databinding.Observable
import androidx.databinding.ObservableField
import com.topjohnwu.magisk.model.observer.Observer
import java.io.Serializable
/**
* Kotlin version of [ObservableField].
* You can define if wrapped type is Nullable or not.
* You can use kotlin get/set syntax for value
*
* ## Notes
* This stays final for fuck's sake. Too many things depend on it, so you just cannot go around and
* change it randomly. Even though you think you're improving the design, you might be fucking this
* up in unimaginable ways. So DON'T TOUCH THIS.
*
* In order to have value-less observer you need - you guessed it - **a fucking [Observer]**!
*/
open class KObservableField<T> : ObservableField<T>, Serializable {
class KObservableField<T> : ObservableField<T>, Serializable {
var value: T
get() = get()
set(value) { set(value) }
set(value) {
if (field != value) {
field = value
notifyChange()
}
}
constructor(init: T) {
value = init
......@@ -23,8 +35,23 @@ open class KObservableField<T> : ObservableField<T>, Serializable {
value = init
}
@Suppress("UNCHECKED_CAST")
@Deprecated(
message = "Needed for data binding, use KObservableField.value syntax from code",
replaceWith = ReplaceWith("value")
)
override fun get(): T {
return super.get() as T
return value
}
@Deprecated(
message = "Needed for data binding, use KObservableField.value = ... syntax from code",
replaceWith = ReplaceWith("value = newValue")
)
override fun set(newValue: T) {
value = newValue
}
override fun toString(): String {
return "KObservableField(value=$value)"
}
}
package com.topjohnwu.magisk.utils
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
import androidx.core.animation.addListener
import androidx.core.text.layoutDirection
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.marginEnd
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.google.android.material.circularreveal.CircularRevealCompat
import com.google.android.material.circularreveal.CircularRevealWidget
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.topjohnwu.magisk.core.utils.currentLocale
import kotlin.math.hypot
object MotionRevealHelper {
fun <CV> withViews(
revealable: CV,
fab: FloatingActionButton,
expanded: Boolean
) where CV : CircularRevealWidget, CV : View {
revealable.revealInfo = revealable.createRevealInfo(!expanded)
val revealInfo = revealable.createRevealInfo(expanded)
val revealAnim = revealable.createRevealAnim(revealInfo)
val moveAnim = fab.createMoveAnim(revealInfo)
AnimatorSet().also {
if (expanded) {
it.play(revealAnim).after(moveAnim)
} else {
it.play(moveAnim).after(revealAnim)
}
}.start()
}
private fun <CV> CV.createRevealAnim(
revealInfo: CircularRevealWidget.RevealInfo
): Animator where CV : CircularRevealWidget, CV : View =
CircularRevealCompat.createCircularReveal(
this,
revealInfo.centerX,
revealInfo.centerY,
revealInfo.radius
).apply {
addListener(onStart = {
isVisible = true
}, onEnd = {
if (revealInfo.radius == 0f) {
isInvisible = true
}
})
}
private fun FloatingActionButton.createMoveAnim(
revealInfo: CircularRevealWidget.RevealInfo
): Animator = AnimatorSet().also {
it.interpolator = FastOutSlowInInterpolator()
it.addListener(onStart = { show() }, onEnd = { if (revealInfo.radius != 0f) hide() })
val rtlMod = if (currentLocale.layoutDirection == View.LAYOUT_DIRECTION_RTL) 1f else -1f
val maxX = revealInfo.centerX - marginEnd - measuredWidth / 2f
val targetX = if (revealInfo.radius == 0f) 0f else maxX * rtlMod
val moveX = ObjectAnimator.ofFloat(this, View.TRANSLATION_X, targetX)
val maxY = revealInfo.centerY - marginBottom - measuredHeight / 2f
val targetY = if (revealInfo.radius == 0f) 0f else -maxY
val moveY = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, targetY)
it.playTogether(moveX, moveY)
}
private fun View.createRevealInfo(expanded: Boolean): CircularRevealWidget.RevealInfo {
val cX = measuredWidth / 2f
val cY = measuredHeight / 2f - paddingBottom
return CircularRevealWidget.RevealInfo(cX, cY, if (expanded) hypot(cX, cY) else 0f)
}
}
package com.topjohnwu.magisk.utils
import android.view.ScaleGestureDetector
abstract class PinchGestureCallback : ScaleGestureDetector.SimpleOnScaleGestureListener() {
private var startFactor: Float = 1f
override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
startFactor = detector?.scaleFactor ?: 1f
return super.onScaleBegin(detector)
}
override fun onScaleEnd(detector: ScaleGestureDetector?) {
val endFactor = detector?.scaleFactor ?: 1f
if (endFactor > startFactor) onZoom()
else if (endFactor < startFactor) onPinch()
}
abstract fun onPinch()
abstract fun onZoom()
}
\ No newline at end of file
package com.topjohnwu.magisk.utils
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import androidx.transition.TransitionManager
import com.topjohnwu.magisk.core.Config
import kotlin.math.max
import kotlin.math.min
class PinchZoomTouchListener private constructor(
private val view: RecyclerView,
private val max: Int = 3,
private val min: Int = 1
) : View.OnTouchListener {
private val layoutManager
get() = view.layoutManager
private val pinchListener = object : PinchGestureCallback() {
override fun onPinch() = updateSpanCount(Config.listSpanCount + 1)
override fun onZoom() = updateSpanCount(Config.listSpanCount - 1)
}
private val gestureDetector by lazy { ScaleGestureDetector(view.context, pinchListener) }
init {
updateSpanCount(Config.listSpanCount, false)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
gestureDetector.onTouchEvent(event)
return false
}
private fun updateSpanCount(count: Int, animate: Boolean = true) {
if (animate) {
TransitionManager.beginDelayedTransition(view)
}
val boundCount = max(min, min(max, count))
when (val l = layoutManager) {
is StaggeredGridLayoutManager -> l.spanCount = boundCount
is GridLayoutManager -> l.spanCount = boundCount
else -> Unit
}
Config.listSpanCount = boundCount
}
companion object {
@SuppressLint("ClickableViewAccessibility")
fun attachTo(view: RecyclerView) = view.setOnTouchListener(PinchZoomTouchListener(view))
fun clear(view: View) = view.setOnTouchListener(null)
}
}
package com.topjohnwu.magisk.utils
import android.content.res.Resources
import android.widget.TextView
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
sealed class TransitiveText {
abstract val isEmpty: Boolean
abstract fun getText(resources: Resources): CharSequence
// ---
class String(
private val value: CharSequence
) : TransitiveText() {
override val isEmpty = value.isEmpty()
override fun getText(resources: Resources) = value
}
class Res(
private val value: Int,
private vararg val params: Any
) : TransitiveText() {
override val isEmpty = value == 0
override fun getText(resources: Resources) =
resources.getString(value, *params)
}
// ---
companion object {
val EMPTY = String("")
}
}
fun Int.asTransitive(vararg params: Any) = TransitiveText.Res(this, *params)
fun CharSequence.asTransitive() = TransitiveText.String(this)
@BindingAdapter("android:text")
fun TextView.setText(text: TransitiveText) {
this.text = text.getText(resources)
}
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
fun TextView.getTransitiveText() = text.asTransitive()
package com.topjohnwu.magisk.view
import android.content.Context
import android.content.DialogInterface
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatDialog
import androidx.core.view.ViewCompat
import androidx.core.view.updatePadding
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.databinding.DialogMagiskBaseBinding
import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.magisk.utils.KObservableField
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapters
import me.tatarka.bindingcollectionadapter2.ItemBinding
class MagiskDialog @JvmOverloads constructor(
context: Context, theme: Int = 0
) : AppCompatDialog(context, theme) {
private val binding: DialogMagiskBaseBinding =
DialogMagiskBaseBinding.inflate(LayoutInflater.from(context))
private val data = Data()
init {
binding.setVariable(BR.data, data)
setCancelable(true)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
super.setContentView(binding.root)
window?.apply {
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT
)
}
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets ->
view.updatePadding(
top = view.paddingTop + insets.systemWindowInsetTop,
bottom = view.paddingBottom + insets.systemWindowInsetBottom
)
insets
}
}
override fun setCancelable(flag: Boolean) {
val listener = if (!flag) {
null
} else {
setCanceledOnTouchOutside(true)
View.OnClickListener { dismiss() }
}
binding.dialogBaseOutsideContainer.setOnClickListener(listener)
}
inner class Data {
val icon = KObservableField(0)
val iconRaw = KObservableField<Drawable?>(null)
val title = KObservableField<CharSequence>("")
val message = KObservableField<CharSequence>("")
val buttonPositive = Button()
val buttonNeutral = Button()
val buttonNegative = Button()
val buttonIDGAF = Button()
}
enum class ButtonType {
POSITIVE, NEUTRAL, NEGATIVE, IDGAF
}
inner class Button {
val icon = KObservableField(0)
val title = KObservableField<CharSequence>("")
val isEnabled = KObservableField(true)
var onClickAction: OnDialogButtonClickListener = {}
var preventDismiss = false
fun clicked() {
//we might not want the click to dismiss the button to begin with
var prevention = preventDismiss
onClickAction(this@MagiskDialog)
//in case we don't want the dialog to close after clicking the button
//ie. the input is incorrect ...
//otherwise we disregard the request, bcs it just might reset the button in the new
//instance
if (preventDismiss) {
prevention = preventDismiss
}
if (!prevention) {
dismiss()
}
}
}
inner class ButtonBuilder(private val button: Button) {
var icon: Int
get() = button.icon.value
set(value) {
button.icon.value = value
}
var title: CharSequence
get() = button.title.value
set(value) {
button.title.value = value
}
var titleRes: Int
get() = 0
set(value) {
button.title.value = context.getString(value)
}
var isEnabled: Boolean
get() = button.isEnabled.value
set(value) {
button.isEnabled.value = value
}
var preventDismiss: Boolean
get() = button.preventDismiss
set(value) {
button.preventDismiss = value
}
fun onClick(listener: OnDialogButtonClickListener) {
button.onClickAction = listener
}
}
fun applyTitle(@StringRes stringRes: Int) =
apply { data.title.value = context.getString(stringRes) }
fun applyTitle(title: CharSequence) =
apply { data.title.value = title }
fun applyMessage(@StringRes stringRes: Int, vararg args: Any) =
apply { data.message.value = context.getString(stringRes, *args) }
fun applyMessage(message: CharSequence) =
apply { data.message.value = message }
fun applyIcon(@DrawableRes drawableRes: Int) =
apply { data.icon.value = drawableRes }
fun applyIcon(drawable: Drawable) =
apply { data.iconRaw.value = drawable }
fun applyButton(buttonType: ButtonType, builder: ButtonBuilder.() -> Unit) = apply {
val button = when (buttonType) {
ButtonType.POSITIVE -> data.buttonPositive
ButtonType.NEUTRAL -> data.buttonNeutral
ButtonType.NEGATIVE -> data.buttonNegative
ButtonType.IDGAF -> data.buttonIDGAF
}
ButtonBuilder(button).apply(builder)
}
class DialogItem(
val item: CharSequence,
val position: Int
) : ComparableRvItem<DialogItem>() {
override val layoutRes = R.layout.item_list_single_line
override fun itemSameAs(other: DialogItem) = item == other.item
override fun contentSameAs(other: DialogItem) = itemSameAs(other)
}
interface ActualOnDialogClickListener {
fun onClick(position: Int)
}
fun applyAdapter(
list: Array<out CharSequence>,
listener: OnDialogClickListener
) = applyView(
RecyclerView(context).also {
it.isNestedScrollingEnabled = false
it.layoutManager = LinearLayoutManager(context)
val actualListener = object : ActualOnDialogClickListener {
override fun onClick(position: Int) {
listener(position)
dismiss()
}
}
val items = list.mapIndexed { i, it -> DialogItem(it, i) }
val binding = itemBindingOf<DialogItem> { it.bindExtra(BR.listener, actualListener) }
.let { ItemBinding.of(it) }
BindingRecyclerViewAdapters.setAdapter(it, binding, items, null, null, null, null)
}
)
fun cancellable(isCancellable: Boolean) = apply {
setCancelable(isCancellable)
}
fun <Binding : ViewDataBinding> applyView(
binding: Binding,
body: Binding.() -> Unit = {}
) = applyView(binding.root).also { binding.apply(body) }
fun applyView(view: View) = apply {
resetView()
binding.dialogBaseContainer.addView(
view,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
fun onDismiss(callback: OnDialogButtonClickListener) =
apply { setOnDismissListener(callback) }
fun onShow(callback: OnDialogButtonClickListener) =
apply { setOnShowListener(callback) }
fun reveal() = apply { super.show() }
// ---
fun resetView() = apply {
binding.dialogBaseContainer.removeAllViews()
}
fun resetTitle() = applyTitle("")
fun resetMessage() = applyMessage("")
fun resetIcon() = applyIcon(0)
fun resetButtons() = apply {
ButtonType.values().forEach {
applyButton(it) {
title = ""
icon = 0
isEnabled = true
preventDismiss = false
onClick {}
}
}
}
fun reset() = resetTitle()
.resetMessage()
.resetView()
.resetIcon()
.resetButtons()
//region Deprecated Members
@Deprecated("Use applyTitle instead", ReplaceWith("applyTitle"))
override fun setTitle(title: CharSequence?) = Unit
@Deprecated("Use applyTitle instead", ReplaceWith("applyTitle"))
override fun setTitle(titleId: Int) = Unit
@Deprecated("Use reveal()", ReplaceWith("reveal()"))
override fun show() {
}
//endregion
}
typealias OnDialogButtonClickListener = (DialogInterface) -> Unit
typealias OnDialogClickListener = (position: Int) -> Unit
......@@ -3,7 +3,6 @@ package com.topjohnwu.magisk.view
import android.content.Context
import android.view.LayoutInflater
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.extensions.subscribeK
......@@ -34,7 +33,8 @@ object MarkDownWindow : KoinComponent {
}
fun show(activity: Context, title: String?, content: Single<String>) {
val mv = LayoutInflater.from(activity).inflate(R.layout.markdown_window, null)
val mdRes = R.layout.markdown_window_md2
val mv = LayoutInflater.from(activity).inflate(mdRes, null)
val tv = mv.findViewById<TextView>(R.id.md_txt)
content.map {
......@@ -45,11 +45,14 @@ object MarkDownWindow : KoinComponent {
tv.setText(R.string.download_file_error)
Completable.complete()
}.subscribeK {
AlertDialog.Builder(activity)
.setTitle(title)
.setView(mv)
.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
.show()
MagiskDialog(activity)
.applyTitle(title ?: "")
.applyView(mv)
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = android.R.string.cancel
}
.reveal()
return@subscribeK
}
}
}
package com.topjohnwu.magisk.view.dialogs
import android.content.Context
import android.content.DialogInterface
import android.view.LayoutInflater
import android.view.View
import androidx.annotation.StringRes
import androidx.annotation.StyleRes
import androidx.appcompat.app.AlertDialog
import com.topjohnwu.magisk.databinding.AlertDialogBinding
open class CustomAlertDialog : AlertDialog.Builder {
private var positiveListener: DialogInterface.OnClickListener? = null
private var negativeListener: DialogInterface.OnClickListener? = null
private var neutralListener: DialogInterface.OnClickListener? = null
protected var dialog: AlertDialog? = null
protected var binding: AlertDialogBinding =
AlertDialogBinding.inflate(LayoutInflater.from(context))
init {
super.setView(binding.root)
binding.message.visibility = View.GONE
binding.negative.visibility = View.GONE
binding.positive.visibility = View.GONE
binding.neutral.visibility = View.GONE
binding.buttonPanel.visibility = View.GONE
}
constructor(context: Context) : super(context)
constructor(context: Context, @StyleRes themeResId: Int) : super(context, themeResId)
override fun setView(layoutResId: Int): CustomAlertDialog {
return this
}
override fun setView(view: View): CustomAlertDialog {
return this
}
override fun setMessage(message: CharSequence?): CustomAlertDialog {
binding.message.visibility = View.VISIBLE
binding.message.text = message
return this
}
override fun setMessage(@StringRes messageId: Int): CustomAlertDialog {
return setMessage(context.getString(messageId))
}
override fun setPositiveButton(text: CharSequence, listener: DialogInterface.OnClickListener?): CustomAlertDialog {
binding.buttonPanel.visibility = View.VISIBLE
binding.positive.visibility = View.VISIBLE
binding.positive.text = text
positiveListener = listener
binding.positive.setOnClickListener {
positiveListener?.onClick(dialog, DialogInterface.BUTTON_POSITIVE)
dialog?.dismiss()
}
return this
}
override fun setPositiveButton(@StringRes textId: Int, listener: DialogInterface.OnClickListener?): CustomAlertDialog {
return setPositiveButton(context.getString(textId), listener)
}
override fun setNegativeButton(text: CharSequence, listener: DialogInterface.OnClickListener?): CustomAlertDialog {
binding.buttonPanel.visibility = View.VISIBLE
binding.negative.visibility = View.VISIBLE
binding.negative.text = text
negativeListener = listener
binding.negative.setOnClickListener {
negativeListener?.onClick(dialog, DialogInterface.BUTTON_NEGATIVE)
dialog?.dismiss()
}
return this
}
override fun setNegativeButton(@StringRes textId: Int, listener: DialogInterface.OnClickListener?): CustomAlertDialog {
return setNegativeButton(context.getString(textId), listener)
}
override fun setNeutralButton(text: CharSequence, listener: DialogInterface.OnClickListener?): CustomAlertDialog {
binding.buttonPanel.visibility = View.VISIBLE
binding.neutral.visibility = View.VISIBLE
binding.neutral.text = text
neutralListener = listener
binding.neutral.setOnClickListener {
neutralListener?.onClick(dialog, DialogInterface.BUTTON_NEUTRAL)
dialog?.dismiss()
}
return this
}
override fun setNeutralButton(@StringRes textId: Int, listener: DialogInterface.OnClickListener?): CustomAlertDialog {
return setNeutralButton(context.getString(textId), listener)
}
override fun create(): AlertDialog {
return super.create().apply { dialog = this }
}
override fun show(): AlertDialog {
return create().apply { show() }
}
fun dismiss() {
dialog?.dismiss()
}
}
package com.topjohnwu.magisk.view.dialogs
import android.app.Activity
import android.app.ProgressDialog
import android.widget.Toast
import androidx.core.net.toUri
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.extensions.cachedFile
import com.topjohnwu.magisk.extensions.reboot
import com.topjohnwu.magisk.net.Networking
import com.topjohnwu.magisk.tasks.MagiskInstaller
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.superuser.io.SuFile
import java.io.File
class EnvFixDialog(activity: Activity) : CustomAlertDialog(activity) {
init {
setTitle(R.string.env_fix_title)
setMessage(R.string.env_fix_msg)
setCancelable(true)
setPositiveButton(android.R.string.yes) { _, _ ->
val pd = ProgressDialog.show(activity,
activity.getString(R.string.setup_title),
activity.getString(R.string.setup_msg))
object : MagiskInstaller() {
override fun operations(): Boolean {
installDir = SuFile("/data/adb/magisk")
Shell.su("rm -rf /data/adb/magisk/*").exec()
val zip : File = activity.cachedFile("magisk.zip")
if (!ShellUtils.checkSum("MD5", zip, Info.remote.magisk.md5))
Networking.get(Info.remote.magisk.link).execForFile(zip)
zipUri = zip.toUri()
return extractZip() && Shell.su("fix_env").exec().isSuccess
}
override fun onResult(success: Boolean) {
pd.dismiss()
Utils.toast(if (success) R.string.reboot_delay_toast else R.string.setup_fail, Toast.LENGTH_LONG)
if (success)
UiThreadHandler.handler.postDelayed({ reboot() }, 5000)
}
}.exec()
}
setNegativeButton(android.R.string.no, null)
}
}
package com.topjohnwu.magisk.view.dialogs
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.utils.Utils
internal class InstallMethodDialog(activity: BaseActivity<*, *>, options: List<String>) :
AlertDialog.Builder(activity) {
init {
setTitle(R.string.select_method)
setItems(options.toTypedArray()) { _, idx ->
when (idx) {
0 -> downloadOnly(activity)
1 -> patchBoot(activity)
2 -> flash(activity)
3 -> installInactiveSlot(activity)
}
}
}
private fun flash(activity: BaseActivity<*, *>) = DownloadService(activity) {
subject = DownloadSubject.Magisk(Configuration.Flash.Primary)
}
private fun patchBoot(activity: BaseActivity<*, *>) = activity.withExternalRW {
onSuccess {
Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
val intent = Intent(Intent.ACTION_GET_CONTENT)
.setType("*/*")
.addCategory(Intent.CATEGORY_OPENABLE)
activity.startActivityForResult(intent, Const.ID.SELECT_BOOT) { resultCode, data ->
if (resultCode == Activity.RESULT_OK && data != null) {
DownloadService(activity) {
val safeData = data.data ?: Uri.EMPTY
subject = DownloadSubject.Magisk(Configuration.Patch(safeData))
}
}
}
}
}
private fun downloadOnly(activity: BaseActivity<*, *>) = activity.withExternalRW {
onSuccess {
DownloadService(activity) {
subject = DownloadSubject.Magisk(Configuration.Download)
}
}
}
private fun installInactiveSlot(activity: BaseActivity<*, *>) {
CustomAlertDialog(activity)
.setTitle(R.string.warning)
.setMessage(R.string.install_inactive_slot_msg)
.setCancelable(true)
.setPositiveButton(android.R.string.yes) { _, _ ->
DownloadService(activity) {
subject = DownloadSubject.Magisk(Configuration.Flash.Secondary)
}
}
.setNegativeButton(android.R.string.no, null)
.show()
}
}
package com.topjohnwu.magisk.view.dialogs
import android.net.Uri
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import java.util.*
class MagiskInstallDialog(a: BaseActivity<*, *>) : CustomAlertDialog(a) {
init {
val filename = "Magisk v${Info.remote.magisk.version}" +
"(${Info.remote.magisk.versionCode})"
setTitle(a.getString(R.string.repo_install_title, a.getString(R.string.magisk)))
setMessage(a.getString(R.string.repo_install_msg, filename))
setCancelable(true)
setPositiveButton(R.string.install) { _, _ ->
val options = ArrayList<String>()
options.add(a.getString(R.string.download_zip_only))
options.add(a.getString(R.string.select_patch_file))
if (Shell.rootAccess()) {
options.add(a.getString(R.string.direct_install))
val s = ShellUtils.fastCmd("grep_prop ro.build.ab_update")
if (s.isNotEmpty() && s.toBoolean()) {
options.add(a.getString(R.string.install_inactive_slot))
}
}
InstallMethodDialog(a, options).show()
}
if (Info.remote.magisk.note.isNotEmpty()) {
setNeutralButton(R.string.release_notes) { _, _ ->
if (Info.remote.magisk.note.contains("forum.xda-developers")) {
// Open forum links in browser
Utils.openLink(a, Uri.parse(Info.remote.magisk.note))
} else {
MarkDownWindow.show(a, null, Info.remote.magisk.note)
}
}
}
}
}
package com.topjohnwu.magisk.view.dialogs
import android.app.Activity
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.view.MarkDownWindow
class ManagerInstallDialog(a: Activity) : CustomAlertDialog(a) {
init {
val subject = DownloadSubject.Manager(Configuration.APK.Upgrade)
setTitle(a.getString(R.string.repo_install_title, a.getString(R.string.app_name)))
setMessage(a.getString(R.string.repo_install_msg, subject.title))
setCancelable(true)
setPositiveButton(R.string.install) { _, _ ->
DownloadService(a) { this.subject = subject }
}
if (Info.remote.app.note.isNotEmpty()) {
setNeutralButton(R.string.app_changelog) { _, _ ->
MarkDownWindow.show(a, null, Info.remote.app.note) }
}
}
}
package com.topjohnwu.magisk.view.dialogs
import android.app.Activity
import android.app.ProgressDialog
import android.widget.Toast
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell
class UninstallDialog(activity: Activity) : CustomAlertDialog(activity) {
init {
setTitle(R.string.uninstall_magisk_title)
setMessage(R.string.uninstall_magisk_msg)
setNeutralButton(R.string.restore_img) { _, _ ->
val dialog = ProgressDialog.show(activity,
activity.getString(R.string.restore_img),
activity.getString(R.string.restore_img_msg))
Shell.su("restore_imgs").submit { result ->
dialog.cancel()
if (result.isSuccess) {
Utils.toast(R.string.restore_done, Toast.LENGTH_SHORT)
} else {
Utils.toast(R.string.restore_fail, Toast.LENGTH_LONG)
}
}
}
if (Info.remote.uninstaller.link.isNotEmpty()) {
setPositiveButton(R.string.complete_uninstall) { _, _ ->
DownloadService(activity) {
subject = DownloadSubject.Magisk(Configuration.Uninstall)
}
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:fromAlpha="0"
android:toAlpha="1" />
<scale
android:duration="@android:integer/config_shortAnimTime"
android:fromXScale="0.9"
android:fromYScale="0.9"
android:pivotX="50%p"
android:pivotY="50%p"
android:toXScale="1"
android:toYScale="1" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:fromAlpha="0"
android:toAlpha="1" />
<scale
android:duration="@android:integer/config_shortAnimTime"
android:fromXScale="1.1"
android:fromYScale="1.1"
android:pivotX="50%p"
android:pivotY="50%p"
android:toXScale="1"
android:toYScale="1" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:fromAlpha="1"
android:toAlpha="0" />
<scale
android:duration="@android:integer/config_shortAnimTime"
android:fromXScale="1"
android:fromYScale="1"
android:pivotX="50%p"
android:pivotY="50%p"
android:toXScale="1.1"
android:toYScale="1.1" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:fromAlpha="1"
android:toAlpha="0" />
<scale
android:duration="@android:integer/config_shortAnimTime"
android:fromXScale="1"
android:fromYScale="1"
android:pivotX="50%p"
android:pivotY="50%p"
android:toXScale="0.9"
android:toYScale="0.9" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime">
<translate
android:fromYDelta="100%"
android:toYDelta="0%" />
<alpha
android:fromAlpha="0"
android:toAlpha="1" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime">
<translate
android:fromYDelta="0%"
android:toYDelta="-100%" />
<alpha
android:fromAlpha="1"
android:toAlpha="0" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorPrimary" android:state_checked="true" />
<item android:color="?colorDisabled" />
</selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorPrimary" android:state_selected="true" />
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorError" />
</selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorError" />
</selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorDisabledVariant" android:state_enabled="false" />
<item android:color="?colorSecondary" android:state_checked="true" />
<item android:color="?colorOnSurfaceVariant" />
</selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorOnPrimary" />
</selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorError" android:state_selected="true" />
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorPrimary" />
</selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorPrimary" />
</selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorSecondary" />
</selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorPrimary" android:state_selected="true" />
<item android:color="?colorPrimary" android:state_checked="true" />
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorOnSurfaceVariant" />
</selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorOnSurface" />
</selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="@color/su_request_background"/>
<shape android:shape="rectangle">
<solid android:color="?colorSurfaceVariant" />
</shape>
</item>
<item>
<inset
android:drawable="@drawable/ic_cloud_download"
android:inset="13dp"/>
<item android:bottom="2dp">
<shape android:shape="rectangle">
<solid android:color="?colorSurface" />
</shape>
</item>
</layer-list>
\ No newline at end of file
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="45"
android:endColor="?colorSurface"
android:startColor="?colorSurfaceVariant" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:height="4dp" />
<corners android:radius="2dp" />
<solid android:color="?colorOnPrimary" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?colorPrimary">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<corners android:radius="@dimen/r1" />
<solid android:color="?colorPrimary" />
</shape>
</item>
</ripple>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?colorPrimary" />
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="270"
android:endColor="?colorSurfaceVariant"
android:startColor="@android:color/transparent" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="@dimen/r1" />
<solid android:color="?colorOnSurfaceVariant" />
<size android:width="5dp" android:height="5dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="@dimen/r1" />
<solid android:color="?colorOnSurfaceVariant" />
<size android:width="5dp" android:height="5dp" />
</shape>
</item>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<solid android:color="@android:color/transparent" />
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<solid android:color="?colorSecondary" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<solid android:color="?colorPrimary" />
</shape>
</clip>
</item>
</layer-list>
\ No newline at end of file
<?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_cloud_download"
android:inset="30%" />
</foreground>
</adaptive-icon>
\ No newline at end of file
<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>
<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>
<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>
<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>
<?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="18dp"
android:height="18dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:name="group"
android:pivotX="12"
android:pivotY="12">
<path
android:name="path"
android:fillColor="#000"
android:pathData="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 Z" />
</group>
</vector>
</aapt:attr>
<target android:name="group">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleX"
android:valueFrom="1"
android:valueTo="0.8"
android:valueType="floatType" />
<objectAnimator
android:duration="200"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleY"
android:valueFrom="1"
android:valueTo="0.8"
android:valueType="floatType" />
<objectAnimator
android:duration="200"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="scaleX"
android:startOffset="200"
android:valueFrom="0.8"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="200"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="scaleY"
android:startOffset="200"
android:valueFrom="0.8"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="100"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="200"
android:valueFrom="0.3"
android:valueTo="1"
android:valueType="floatType" />
</aapt:attr>
</target>
</animated-vector>
<?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="18dp"
android:height="18dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:name="group"
android:pivotX="12"
android:pivotY="12">
<path
android:name="path"
android:fillAlpha="0.7"
android:fillColor="#000"
android:pathData="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 Z" />
</group>
</vector>
</aapt:attr>
<target android:name="group">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleX"
android:valueFrom="1"
android:valueTo="0.8"
android:valueType="floatType" />
<objectAnimator
android:duration="200"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleY"
android:valueFrom="1"
android:valueTo="0.8"
android:valueType="floatType" />
<objectAnimator
android:duration="200"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="scaleX"
android:startOffset="200"
android:valueFrom="0.8"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="200"
android:interpolator="@android:anim/anticipate_overshoot_interpolator"
android:propertyName="scaleY"
android:startOffset="200"
android:valueFrom="0.8"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="100"
android:interpolator="@interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="200"
android:valueFrom="1"
android:valueTo="0.3"
android:valueType="floatType" />
</aapt:attr>
</target>
</animated-vector>
<?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>
<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>
<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>
<?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>
<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>
<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>
<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>
<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>
<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>
<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>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/defColorSurfaceVariant" />
</shape>
</item>
<item android:bottom="2dp">
<shape android:shape="rectangle">
<solid android:color="@color/defColorSurface" />
</shape>
</item>
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="45"
android:endColor="@color/defColorSurface"
android:startColor="@color/defColorSurfaceVariant" />
</shape>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:height="4dp" />
<corners android:radius="2dp" />
<solid android:color="@color/defColorOnPrimary" />
</shape>
<?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="?colorSecondary" />
<corners android:bottomLeftRadius="2dp" android:bottomRightRadius="2dp" />
</shape>
</item>
</selector>
\ No newline at end of file
<?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="?colorSecondary" />
<corners android:topLeftRadius="2dp" android:topRightRadius="2dp" />
</shape>
</item>
</selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="@dimen/r1" />
<solid android:color="@color/defColorPrimaryVariant" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
</shape>
</item>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="@dimen/r1" />
<solid android:color="@color/defColorPrimaryVariant" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
</shape>
</item>
</selector>
<?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>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="270"
android:endColor="@color/defColorSurfaceVariant"
android:startColor="@android:color/transparent" />
</shape>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size
android:width="@dimen/l1"
android:height="@dimen/l1" />
<solid android:color="@android:color/transparent" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size
android:width="@dimen/l_50"
android:height="@dimen/l_50" />
<solid android:color="@android:color/transparent" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="@dimen/r1" />
<solid android:color="@color/defColorOnSurfaceVariant" />
<size android:width="5dp" android:height="5dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="@dimen/r1" />
<solid android:color="@color/defColorOnSurfaceVariant" />
<size android:width="5dp" android:height="5dp" />
</shape>
</item>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
</shape>
</item>
</selector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
<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>
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
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"/>
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>
<?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>
<?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>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#fff"
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#fff"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>
<?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>
\ No newline at end of file
<?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>
\ No newline at end of file
<?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>
\ No newline at end of file
<?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>
\ No newline at end of file
<?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>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/dark"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM17,13l-5,5 -5,-5h3V9h4v4h3z"/>
</vector>
<?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>
\ No newline at end of file
<?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>
\ No newline at end of file
<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>
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" />
<path
android:pathData="M0-0.75h24v24H0z" />
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>
\ No newline at end of file
<?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="M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z"
tools:fillColor="#000" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#000"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>
<?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="M6,13H18V11H6M3,6V8H21V6M10,18H14V16H10V18Z"
tools:fillColor="@android:color/black" />
</vector>
\ No newline at end of file
<?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,18H4V8H20M20,6H12L10,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8A2,2 0 0,0 20,6M15,16H6V14H15V16M18,12H6V10H18V12Z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:name="forth"
android:pivotX="12"
android:pivotY="12"
android:rotation="180">
<path
android:fillColor="#000"
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" />
</group>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#fff"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
</vector>
<?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"
tools:fillColor="#000"
android:pathData="M2,5.27L3.28,4L20,20.72L18.73,22L15.65,18.92C14.5,19.3 13.28,19.5 12,19.5C7,19.5 2.73,16.39 1,12C1.69,10.24 2.79,8.69 4.19,7.46L2,5.27M12,9A3,3 0 0,1 15,12C15,12.35 14.94,12.69 14.83,13L11,9.17C11.31,9.06 11.65,9 12,9M12,4.5C17,4.5 21.27,7.61 23,12C22.18,14.08 20.79,15.88 19,17.19L17.58,15.76C18.94,14.82 20.06,13.54 20.82,12C19.17,8.64 15.76,6.5 12,6.5C10.91,6.5 9.84,6.68 8.84,7L7.3,5.47C8.74,4.85 10.33,4.5 12,4.5M3.18,12C4.83,15.36 8.24,17.5 12,17.5C12.69,17.5 13.37,17.43 14,17.29L11.72,15C10.29,14.85 9.15,13.71 9,12.28L5.6,8.87C4.61,9.72 3.78,10.78 3.18,12Z" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_show_md2" android:state_selected="true" />
<item android:drawable="@drawable/ic_hide_md2" />
</selector>
\ No newline at end of file
<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="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" />
</vector>
<?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_home_filled_md2"
android:state_checked="true" />
<item
android:id="@+id/unchecked"
android:drawable="@drawable/ic_home_outlined_md2" />
<transition
android:drawable="@drawable/avd_home_from_filled"
android:fromId="@+id/checked"
android:toId="@+id/unchecked" />
<transition
android:drawable="@drawable/avd_home_to_filled"
android:fromId="@+id/unchecked"
android:toId="@id/checked" />
</animated-selector>
\ No newline at end of file
<?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: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" />
</vector>
\ No newline at end of file
<?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="M21 11.1V8C21 6.9 20.1 6 19 6H11L9 4H3C1.9 4 1 4.9 1 6V18C1 19.1 1.9 20 3 20H10.2C11.4 21.8 13.6 23 16 23C19.9 23 23 19.9 23 16C23 14.1 22.2 12.4 21 11.1M9.3 18H3V8H19V9.7C18.1 9.2 17.1 9 16 9C12.1 9 9 12.1 9 16C9 16.7 9.1 17.4 9.3 18M16 21C13.2 21 11 18.8 11 16S13.2 11 16 11 21 13.2 21 16 18.8 21 16 21M17 14H15V12H17V14M17 20H15V15H17V20Z" />
</vector>
\ No newline at end of file
<?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,18L7,13H10V9H14V13H17L12,18M10,2H14A2,2 0 0,1 16,4V6H20A2,2 0 0,1 22,8V19A2,2 0 0,1 20,21H4C2.89,21 2,20.1 2,19V8C2,6.89 2.89,6 4,6H8V4C8,2.89 8.89,2 10,2M14,6V4H10V6H14M4,8V19H20V8H4Z" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/collapsed"
android:drawable="@drawable/ic_delete_md2"
android:state_selected="true" />
<item
android:id="@+id/expanded"
android:drawable="@drawable/ic_magisk_outline" />
<transition
android:drawable="@drawable/avd_magisk_delete"
android:fromId="@+id/expanded"
android:toId="@+id/collapsed" />
<transition
android:drawable="@drawable/avd_delete_magisk"
android:fromId="@+id/collapsed"
android:toId="@id/expanded" />
</animated-selector>
\ No newline at end of file
<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,3v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1L9,4h10v16L9,20v-1c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v2c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L21,3c0,-1.1 -0.9,-2 -2,-2L9,1c-1.1,0 -2,0.9 -2,2zM9.5,15.5c0.29,-0.12 0.55,-0.29 0.8,-0.48l-0.02,0.03 1.01,0.39c0.23,0.09 0.49,0 0.61,-0.22l0.84,-1.46c0.12,-0.21 0.07,-0.49 -0.12,-0.64l-0.85,-0.68 -0.02,0.03c0.02,-0.16 0.05,-0.32 0.05,-0.48s-0.03,-0.32 -0.05,-0.48l0.02,0.03 0.85,-0.68c0.19,-0.15 0.24,-0.43 0.12,-0.64l-0.84,-1.46c-0.12,-0.21 -0.38,-0.31 -0.61,-0.22l-1.01,0.39 0.02,0.03c-0.25,-0.17 -0.51,-0.34 -0.8,-0.46l-0.17,-1.08C9.3,7.18 9.09,7 8.84,7L7.16,7c-0.25,0 -0.46,0.18 -0.49,0.42L6.5,8.5c-0.29,0.12 -0.55,0.29 -0.8,0.48l0.02,-0.03 -1.02,-0.39c-0.23,-0.09 -0.49,0 -0.61,0.22l-0.84,1.46c-0.12,0.21 -0.07,0.49 0.12,0.64l0.85,0.68 0.02,-0.03c-0.02,0.15 -0.05,0.31 -0.05,0.47s0.03,0.32 0.05,0.48l-0.02,-0.03 -0.85,0.68c-0.19,0.15 -0.24,0.43 -0.12,0.64l0.84,1.46c0.12,0.21 0.38,0.31 0.61,0.22l1.01,-0.39 -0.01,-0.04c0.25,0.19 0.51,0.36 0.8,0.48l0.17,1.07c0.03,0.25 0.24,0.43 0.49,0.43h1.68c0.25,0 0.46,-0.18 0.49,-0.42l0.17,-1.08zM6,12c0,-1.1 0.9,-2 2,-2s2,0.9 2,2 -0.9,2 -2,2 -2,-0.9 -2,-2z" />
</vector>
<?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.5,11H19V7C19,5.89 18.1,5 17,5H13V3.5A2.5,2.5 0 0,0 10.5,1A2.5,2.5 0 0,0 8,3.5V5H4A2,2 0 0,0 2,7V10.8H3.5C5,10.8 6.2,12 6.2,13.5C6.2,15 5,16.2 3.5,16.2H2V20A2,2 0 0,0 4,22H7.8V20.5C7.8,19 9,17.8 10.5,17.8C12,17.8 13.2,19 13.2,20.5V22H17A2,2 0 0,0 19,20V16H20.5A2.5,2.5 0 0,0 23,13.5A2.5,2.5 0 0,0 20.5,11Z" />
</vector>
\ No newline at end of file
<?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_module_filled_md2"
android:state_checked="true" />
<item
android:id="@+id/unchecked"
android:drawable="@drawable/ic_module_outlined_md2" />
<transition
android:drawable="@drawable/avd_module_from_filled"
android:fromId="@+id/checked"
android:toId="@+id/unchecked" />
<transition
android:drawable="@drawable/avd_module_to_filled"
android:fromId="@+id/unchecked"
android:toId="@id/checked" />
</animated-selector>
\ No newline at end of file
<?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="M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z" />
</vector>
\ No newline at end of file
<?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="M2,10.96C1.5,10.68 1.35,10.07 1.63,9.59L3.13,7C3.24,6.8 3.41,6.66 3.6,6.58L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.66,6.72 20.82,6.88 20.91,7.08L22.36,9.6C22.64,10.08 22.47,10.69 22,10.96L21,11.54V16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V10.96C2.7,11.13 2.32,11.14 2,10.96M12,4.15V4.15L12,10.85V10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V12.69L14,15.59C13.67,15.77 13.3,15.76 13,15.6V19.29L19,15.91M13.85,13.36L20.13,9.73L19.55,8.72L13.27,12.35L13.85,13.36Z" />
</vector>
\ No newline at end of file
<?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.75,4.09L15.22,6.03L16.13,9.09L13.5,7.28L10.87,9.09L11.78,6.03L9.25,4.09L12.44,4L13.5,1L14.56,4L17.75,4.09M21.25,11L19.61,12.25L20.2,14.23L18.5,13.06L16.8,14.23L17.39,12.25L15.75,11L17.81,10.95L18.5,9L19.19,10.95L21.25,11M18.97,15.95C19.8,15.87 20.69,17.05 20.16,17.8C19.84,18.25 19.5,18.67 19.08,19.07C15.17,23 8.84,23 4.94,19.07C1.03,15.17 1.03,8.83 4.94,4.93C5.34,4.53 5.76,4.17 6.21,3.85C6.96,3.32 8.14,4.21 8.06,5.04C7.79,7.9 8.75,10.87 10.95,13.06C13.14,15.26 16.1,16.22 18.97,15.95M17.33,17.97C14.5,17.81 11.7,16.64 9.53,14.5C7.36,12.31 6.2,9.5 6.04,6.68C3.23,9.82 3.34,14.64 6.35,17.66C9.37,20.67 14.19,20.78 17.33,17.97Z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7.58,4.08L6.15,2.65C3.75,4.48 2.17,7.3 2.03,10.5h2c0.15,-2.65 1.51,-4.97 3.55,-6.42zM19.97,10.5h2c-0.15,-3.2 -1.73,-6.02 -4.12,-7.85l-1.42,1.43c2.02,1.45 3.39,3.77 3.54,6.42zM18,11c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2v-5zM12,22c0.14,0 0.27,-0.01 0.4,-0.04 0.65,-0.14 1.18,-0.58 1.44,-1.18 0.1,-0.24 0.15,-0.5 0.15,-0.78h-4c0.01,1.1 0.9,2 2.01,2z"/>
</vector>
<?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="M16,17H7V10.5C7,8 9,6 11.5,6C14,6 16,8 16,10.5M18,16V10.5C18,7.43 15.86,4.86 13,4.18V3.5A1.5,1.5 0 0,0 11.5,2A1.5,1.5 0 0,0 10,3.5V4.18C7.13,4.86 5,7.43 5,10.5V16L3,18V19H20V18M11.5,22A2,2 0 0,0 13.5,20H9.5A2,2 0 0,0 11.5,22Z" />
</vector>
\ No newline at end of file
<?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="?colorError"
android:pathData="M22.11 21.46L2.39 1.73L1.11 3L4.06 5.95C2.78 7.63 2 9.72 2 12C2 17.5 6.5 22 12 22C14.28 22 16.37 21.23 18.05 19.94L20.84 22.73L22.11 21.46M12 20C7.58 20 4 16.42 4 12C4 10.27 4.56 8.68 5.5 7.38L16.62 18.5C15.32 19.45 13.73 20 12 20M8.17 4.97L6.72 3.5C8.25 2.56 10.06 2 12 2C17.5 2 22 6.5 22 12C22 13.94 21.44 15.75 20.5 17.28L19.03 15.83C19.65 14.69 20 13.39 20 12C20 7.58 16.42 4 12 4C10.61 4 9.31 4.35 8.17 4.97Z" />
</vector>
\ No newline at end of file
<?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.78,7C9.08,7.04 10,7.53 10.57,8.46C11.13,9.4 11.41,10.56 11.39,11.95C11.4,13.5 11.09,14.73 10.5,15.62C9.88,16.5 8.95,16.97 7.71,17C6.45,16.96 5.54,16.5 4.96,15.56C4.38,14.63 4.09,13.45 4.09,12C4.09,10.55 4.39,9.36 5,8.44C5.59,7.5 6.5,7.04 7.78,7M7.75,8.63C7.31,8.63 6.96,8.9 6.7,9.46C6.44,10 6.32,10.87 6.32,12C6.31,13.15 6.44,14 6.69,14.54C6.95,15.1 7.31,15.37 7.77,15.37C8.69,15.37 9.16,14.24 9.17,12C9.17,9.77 8.7,8.65 7.75,8.63M13.33,17V15.22L13.76,15.24L14.3,15.22L15.34,15.03C15.68,14.92 16,14.78 16.26,14.58C16.59,14.35 16.86,14.08 17.07,13.76C17.29,13.45 17.44,13.12 17.53,12.78L17.5,12.77C17.05,13.19 16.38,13.4 15.47,13.41C14.62,13.4 13.91,13.15 13.34,12.65C12.77,12.15 12.5,11.43 12.46,10.5C12.47,9.5 12.81,8.69 13.47,8.03C14.14,7.37 15,7.03 16.12,7C17.37,7.04 18.29,7.45 18.88,8.24C19.47,9 19.76,10 19.76,11.19C19.75,12.15 19.61,13 19.32,13.76C19.03,14.5 18.64,15.13 18.12,15.64C17.66,16.06 17.11,16.38 16.47,16.61C15.83,16.83 15.12,16.96 14.34,17H13.33M16.06,8.63C15.65,8.64 15.32,8.8 15.06,9.11C14.81,9.42 14.68,9.84 14.68,10.36C14.68,10.8 14.8,11.16 15.03,11.46C15.27,11.77 15.63,11.92 16.11,11.93C16.43,11.93 16.7,11.86 16.92,11.74C17.14,11.61 17.3,11.46 17.41,11.28C17.5,11.17 17.53,10.97 17.53,10.71C17.54,10.16 17.43,9.69 17.2,9.28C16.97,8.87 16.59,8.65 16.06,8.63M9.25,5L12.5,1.75L15.75,5H9.25M15.75,19L12.5,22.25L9.25,19H15.75Z" />
</vector>
\ No newline at end of file
<?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="M9.25,5L12.5,1.75L15.75,5H9.25M15.75,19L12.5,22.25L9.25,19H15.75M8.89,14.3H6L5.28,17H2.91L6,7H9L12.13,17H9.67L8.89,14.3M6.33,12.68H8.56L7.93,10.56L7.67,9.59L7.42,8.63H7.39L7.17,9.6L6.93,10.58L6.33,12.68M13.05,17V15.74L17.8,8.97V8.91H13.5V7H20.73V8.34L16.09,15V15.08H20.8V17H13.05Z" />
</vector>
\ No newline at end of file
<?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.71,4.63L19.37,3.29C19,2.9 18.35,2.9 17.96,3.29L9,12.25L11.75,15L20.71,6.04C21.1,5.65 21.1,5 20.71,4.63M7,14A3,3 0 0,0 4,17C4,18.31 2.84,19 2,19C2.92,20.22 4.5,21 6,21A4,4 0 0,0 10,17A3,3 0 0,0 7,14Z" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/collapsed"
android:state_checked="true">
<vector
android:name="vector"
android:width="18dp"
android:height="18dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:name="group"
android:pivotX="12"
android:pivotY="12">
<path
android:name="path"
android:fillColor="#000"
android:pathData="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 Z" />
</group>
</vector>
</item>
<item
android:id="@+id/collapsed_selected"
android:state_selected="true">
<vector
android:name="vector"
android:width="18dp"
android:height="18dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:name="group"
android:pivotX="12"
android:pivotY="12">
<path
android:name="path"
android:fillColor="#000"
android:pathData="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 Z" />
</group>
</vector>
</item>
<item android:id="@+id/expanded">
<vector
android:name="vector"
android:width="18dp"
android:height="18dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:name="group"
android:pivotX="12"
android:pivotY="12">
<path
android:name="path"
android:fillAlpha="0.3"
android:fillColor="#000"
android:pathData="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 Z" />
</group>
</vector>
</item>
<transition
android:drawable="@drawable/avd_circle_from_filled"
android:fromId="@+id/expanded"
android:toId="@+id/collapsed" />
<transition
android:drawable="@drawable/avd_circle_to_filled"
android:fromId="@+id/collapsed"
android:toId="@id/expanded" />
<transition
android:drawable="@drawable/avd_circle_from_filled"
android:fromId="@+id/expanded"
android:toId="@+id/collapsed_selected" />
<transition
android:drawable="@drawable/avd_circle_to_filled"
android:fromId="@+id/collapsed_selected"
android:toId="@id/expanded" />
</animated-selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="?colorOnSurface" android:pathData="M19,12V13.5A4,4 0 0,1 23,17.5C23,18.32 22.75,19.08 22.33,19.71L21.24,18.62C21.41,18.28 21.5,17.9 21.5,17.5A2.5,2.5 0 0,0 19,15V16.5L16.75,14.25L19,12M19,23V21.5A4,4 0 0,1 15,17.5C15,16.68 15.25,15.92 15.67,15.29L16.76,16.38C16.59,16.72 16.5,17.1 16.5,17.5A2.5,2.5 0 0,0 19,20V18.5L21.25,20.75L19,23M12,3C16.42,3 20,4.79 20,7C20,9.21 16.42,11 12,11C7.58,11 4,9.21 4,7C4,4.79 7.58,3 12,3M4,9C4,11.21 7.58,13 12,13C13.11,13 14.17,12.89 15.14,12.68C14.19,13.54 13.5,14.67 13.18,15.96L12,16C7.58,16 4,14.21 4,12V9M20,9V11H19.5L18.9,11.03C19.6,10.43 20,9.74 20,9M4,14C4,16.21 7.58,18 12,18L13,17.97C13.09,19.03 13.42,20 13.95,20.88L12,21C7.58,21 4,19.21 4,17V14Z" />
</vector>
\ No newline at end of file
<?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 21C8.25 20 5 15.54 5 11.22V6.3L12 3.18L19 6.3V12.07A6.45 6.45 0 0 1 20.91 12.67A11.63 11.63 0 0 0 21 11V5L12 1L3 5V11C3 16.55 6.84 21.74 12 23C12.35 22.91 12.7 22.8 13 22.68A6.3 6.3 0 0 1 12 21M18 14.5V13L15.75 15.25L18 17.5V16A2.5 2.5 0 0 1 20.24 19.62L21.33 20.71A4 4 0 0 0 18 14.5M18 21A2.5 2.5 0 0 1 15.76 17.38L14.67 16.29A4 4 0 0 0 18 22.5V24L20.25 21.75L18 19.5Z" />
</vector>
\ No newline at end of file
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94L12,12L5,12L5,6.3l7,-3.11v8.8z"/>
</vector>
<?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="M13.62,13.28L15.26,15.54C15.29,15 15.38,14.47 15.56,13.93C15.74,13.37 16,12.86 16.3,12.41L13.62,13.28M17.77,19C17.75,17.68 17.95,16.27 18.41,14.85C18.88,13.42 19.56,12.14 20.36,11.09L18.32,11.75C17.54,12.26 16.87,13.14 16.5,14.23C16.17,15.29 16.18,16.36 16.5,17.22L17.77,19M19.25,21C18.75,21.46 18.21,21.86 17.63,22.2L17.31,21.76C15.9,21 14.04,20.5 12,20.5C9.96,20.5 8.1,21 6.69,21.76L6.37,22.2C5.79,21.86 5.25,21.46 4.75,21L5,20.69C5.33,19.08 5.22,17.12 4.58,15.14C3.94,13.19 2.9,11.56 1.71,10.46L1.27,10.32C1.42,9.66 1.63,9 1.89,8.41L2.29,8.54C3.92,8.37 5.74,7.65 7.43,6.43C9.09,5.22 10.32,3.73 11,2.26V1.79L12,1.75L13,1.79V2.21C13.67,3.71 14.91,5.23 16.6,6.45C18.26,7.66 20.06,8.37 21.67,8.56L22.11,8.41C22.37,9 22.58,9.66 22.73,10.32L22.23,10.5C21.04,11.58 20,13.21 19.37,15.16C18.74,17.09 18.62,19 18.92,20.57L19.25,21M11,11.37V8.5C10.66,8.92 10.26,9.32 9.78,9.67C9.29,10 8.77,10.29 8.25,10.5L11,11.37M11,4.22C10.25,5.31 9.24,6.35 8,7.24C6.78,8.14 5.46,8.78 4.18,9.16L6.15,9.8C7.11,9.88 8.22,9.56 9.19,8.86C10.13,8.18 10.76,7.27 11,6.36V4.22M13,11.37L15.74,10.5C15.23,10.3 14.72,10.03 14.25,9.69C13.76,9.33 13.34,8.92 13,8.5V11.37M19.8,9.16C18.53,8.78 17.23,8.15 16,7.26C14.77,6.36 13.76,5.31 13,4.2V6.27C13.22,7.21 13.86,8.17 14.84,8.88C15.77,9.56 16.83,9.87 17.77,9.82L19.8,9.16M10.38,13.28L7.64,12.39C7.95,12.84 8.2,13.35 8.38,13.91C8.57,14.5 8.66,15.06 8.68,15.62L10.38,13.28M3.58,11.07C4.38,12.12 5.06,13.4 5.53,14.83C6,16.29 6.2,17.74 6.17,19.08L7.38,17.41C7.76,16.5 7.8,15.36 7.43,14.22C7.07,13.12 6.4,12.24 5.61,11.73L3.58,11.07M12,14.45L10.35,16.72C10.87,16.58 11.42,16.5 12,16.5C12.58,16.5 13.13,16.58 13.65,16.72L12,14.45M7.84,20.18C9.08,19.75 10.5,19.5 12,19.5C13.5,19.5 14.92,19.75 16.16,20.18L14.88,18.42C14.16,17.85 13.13,17.5 12,17.5C10.87,17.5 9.84,17.85 9.12,18.42L7.84,20.18Z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="?attr/colorControl"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>
<?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 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V7L17 3M19 19H5V5H16.17L19 7.83V19M12 12C10.34 12 9 13.34 9 15S10.34 18 12 18 15 16.66 15 15 13.66 12 12 12M6 6H15V10H6V6Z" />
</vector>
\ No newline at end of file
<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="475.084"
android:viewportHeight="475.084">
<group
android:pivotX="237.542"
android:pivotY="237.542"
android:scaleX="0.8"
android:scaleY="0.8">
<path
android:fillColor="?colorOnSurface"
android:pathData="M464.524,412.846l-97.929,-97.925c23.6,-34.068 35.406,-72.047 35.406,-113.917c0,-27.218 -5.284,-53.249 -15.852,-78.087c-10.561,-24.842 -24.838,-46.254 -42.825,-64.241c-17.987,-17.987 -39.396,-32.264 -64.233,-42.826C254.246,5.285 228.217,0.003 200.999,0.003c-27.216,0 -53.247,5.282 -78.085,15.847C98.072,26.412 76.66,40.689 58.673,58.676c-17.989,17.987 -32.264,39.403 -42.827,64.241C5.282,147.758 0,173.786 0,201.004c0,27.216 5.282,53.238 15.846,78.083c10.562,24.838 24.838,46.247 42.827,64.234c17.987,17.993 39.403,32.264 64.241,42.832c24.841,10.563 50.869,15.844 78.085,15.844c41.879,0 79.852,-11.807 113.922,-35.405l97.929,97.641c6.852,7.231 15.406,10.849 25.693,10.849c9.897,0 18.467,-3.617 25.694,-10.849c7.23,-7.23 10.848,-15.796 10.848,-25.693C475.088,428.458 471.567,419.889 464.524,412.846zM291.363,291.358c-25.029,25.033 -55.148,37.549 -90.364,37.549c-35.21,0 -65.329,-12.519 -90.36,-37.549c-25.031,-25.029 -37.546,-55.144 -37.546,-90.36c0,-35.21 12.518,-65.334 37.546,-90.36c25.026,-25.032 55.15,-37.546 90.36,-37.546c35.212,0 65.331,12.519 90.364,37.546c25.033,25.026 37.548,55.15 37.548,90.36C328.911,236.214 316.392,266.329 291.363,291.358z"
tools:fillColor="@android:color/black" />
</group>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
</vector>
<?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,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" />
</vector>
\ No newline at end of file
<?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_settings_filled_md2"
android:state_checked="true" />
<item
android:id="@+id/unchecked"
android:drawable="@drawable/ic_settings_outlined_md2" />
<transition
android:drawable="@drawable/avd_settings_from_filled"
android:fromId="@+id/checked"
android:toId="@+id/unchecked" />
<transition
android:drawable="@drawable/avd_settings_to_filled"
android:fromId="@+id/unchecked"
android:toId="@id/checked" />
</animated-selector>
\ No newline at end of file
<?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,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z" />
</vector>
\ No newline at end of file
<?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="M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9M12,4.5C17,4.5 21.27,7.61 23,12C21.27,16.39 17,19.5 12,19.5C7,19.5 2.73,16.39 1,12C2.73,7.61 7,4.5 12,4.5M3.18,12C4.83,15.36 8.24,17.5 12,17.5C15.76,17.5 19.17,15.36 20.82,12C19.17,8.64 15.76,6.5 12,6.5C8.24,6.5 4.83,8.64 3.18,12Z"
tools:fillColor="#000" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="?attr/colorControl"
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#fff"
android:pathData="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" />
android:fillColor="?colorOnSurface"
android:pathData="M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1Z" />
</vector>
<?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_superuser_filled_md2"
android:state_checked="true" />
<item
android:id="@+id/unchecked"
android:drawable="@drawable/ic_superuser_outlined_md2" />
<transition
android:drawable="@drawable/avd_superuser_from_filled"
android:fromId="@+id/checked"
android:toId="@+id/unchecked" />
<transition
android:drawable="@drawable/avd_superuser_to_filled"
android:fromId="@+id/unchecked"
android:toId="@id/checked" />
</animated-selector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorOnSurface"
android:pathData="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#757575"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
</vector>
<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="512"
android:viewportHeight="512">
<group
android:pivotX="256"
android:pivotY="256"
android:rotation="180">
<path
android:fillColor="?colorOnSurface"
tools:fillColor="#000"
android:pathData="m256.238,430.145 l-89.77,-94.898 29.059,-27.488 60.633,64.098 60.273,-64.059 29.133,27.41zM422,392v-272c0,-66.168 -53.832,-120 -120,-120h-92c-66.168,0 -120,53.832 -120,120v272c0,66.168 53.832,120 120,120h92c66.168,0 120,-53.832 120,-120zM302,40c44.113,0 80,35.887 80,80v272c0,44.113 -35.887,80 -80,80h-92c-44.113,0 -80,-35.887 -80,-80v-272c0,-44.113 35.887,-80 80,-80zM256,81c-11.047,0 -20,8.953 -20,20s8.953,20 20,20 20,-8.953 20,-20 -8.953,-20 -20,-20zM256,161c-11.047,0 -20,8.953 -20,20s8.953,20 20,20 20,-8.953 20,-20 -8.953,-20 -20,-20zM256,241c-11.047,0 -20,8.953 -20,20s8.953,20 20,20 20,-8.953 20,-20 -8.953,-20 -20,-20zM256,241" />
</group>
</vector>
<vector android:height="24dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M8,256C8,119 119,8 256,8s248,111 248,248 -111,248 -248,248S8,393 8,256zM151.6,284.9l72.4,-75.5L224,392c0,13.3 10.7,24 24,24h16c13.3,0 24,-10.7 24,-24L288,209.4l72.4,75.5c9.3,9.7 24.8,9.9 34.3,0.4l10.9,-11c9.4,-9.4 9.4,-24.6 0,-33.9L273,107.7c-9.4,-9.4 -24.6,-9.4 -33.9,0L106.3,240.4c-9.4,9.4 -9.4,24.6 0,33.9l10.9,11c9.6,9.5 25.1,9.3 34.4,-0.4z"/>
</vector>
......@@ -5,6 +5,6 @@
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M16.59,5.59L18,7L12,13L6,7L7.41,5.59L12,10.17L16.59,5.59M16.59,11.59L18,13L12,19L6,13L7.41,11.59L12,16.17L16.59,11.59Z" />
android:fillColor="?colorOnSurface"
android:pathData="M17,1H7A2,2 0 0,0 5,3V21A2,2 0 0,0 7,23H17A2,2 0 0,0 19,21V3A2,2 0 0,0 17,1M17,19H7V5H17V19M16,13H13V8H11V13H8L12,17L16,13Z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFd32f2f"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<solid android:color="@android:color/transparent" />
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<solid android:color="@color/defColorSecondary" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<solid android:color="@color/defColorPrimary" />
</shape>
</clip>
</item>
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<font
android:font="@font/exo_regular"
android:fontStyle="normal"
android:fontWeight="400"
app:font="@font/exo_regular"
app:fontStyle="normal"
app:fontWeight="400" />
<font
android:font="@font/exo_regular_italic"
android:fontStyle="italic"
android:fontWeight="400"
app:font="@font/exo_regular_italic"
app:fontStyle="italic"
app:fontWeight="400" />
<font
android:font="@font/exo_bold"
android:fontStyle="normal"
android:fontWeight="500"
app:font="@font/exo_bold"
app:fontStyle="normal"
app:fontWeight="500" />
<font
android:font="@font/exo_bold_italic"
android:fontStyle="italic"
android:fontWeight="500"
app:font="@font/exo_bold_italic"
app:fontStyle="italic"
app:fontWeight="500" />
</font-family>
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
......@@ -7,7 +7,7 @@
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.flash.FlashViewModel" />
type="com.topjohnwu.magisk.legacy.flash.FlashViewModel" />
</data>
......@@ -149,4 +149,4 @@
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
\ No newline at end of file
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.MainViewModel" />
</data>
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<include
android:id="@+id/main_include"
layout="@layout/activity_main_content"
viewModel="@{viewModel}" />
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
onNavigationClick="@{(item) -> viewModel.navigationItemPressed(item)}"
app:menu="@menu/drawer" />
</androidx.drawerlayout.widget.DrawerLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.MainViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
app:elevation="0dp"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/main_toolbar"
onNavigationClick="@{() -> viewModel.navPressed()}"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/primary"
app:navigationIcon="@drawable/ic_menu"
app:title="@string/magisk"
app:titleTextColor="@android:color/white" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/main_nav_host"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:windowBackground"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.MainViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?colorSurface"
android:orientation="vertical"
android:paddingLeft="@{viewModel.insets.left}"
android:paddingRight="@{viewModel.insets.right}"
tools:ignore="RtlHardcoded">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/main_nav_host"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/main_toolbar_wrapper"
style="@style/WidgetFoundation.Appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@{viewModel.insets.top}">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/main_toolbar"
style="@style/WidgetFoundation.Toolbar"
android:layout_width="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_scrollFlags="noScroll"
tools:layout_marginTop="24dp"
tools:title="Home" />
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/main_bottom_bar"
style="@style/WidgetFoundation.Card.Elevated"
android:layout_width="192dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@{(int) @dimen/l1 + viewModel.insets.bottom}"
tools:layout_marginBottom="64dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/main_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:elevation="0dp"
app:itemIconTint="@color/color_menu_tint"
app:itemTextAppearanceActive="@style/AppearanceFoundation.Tiny.Bold"
app:itemTextAppearanceInactive="@style/AppearanceFoundation.Tiny.Bold"
app:itemTextColor="@color/color_menu_tint"
app:labelVisibilityMode="unlabeled"
app:menu="@menu/menu_bottom_nav" />
<androidx.appcompat.widget.AppCompatTextView
gone="@{viewModel.isConnected}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingBottom="@dimen/l_50"
android:text="@string/no_connection"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:textColor="?colorError"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
......@@ -7,7 +7,7 @@
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.surequest.SuRequestViewModel" />
type="com.topjohnwu.magisk.legacy.surequest.SuRequestViewModel" />
</data>
......
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.request.RequestViewModel" />
</data>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.core.widget.NestedScrollView>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:id="@+id/dialog_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="5dp">
<TextView
android:id="@+id/message"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="25dp"
android:paddingTop="12dp"
android:paddingEnd="25dp"
android:paddingBottom="12dp"
android:textColor="?android:attr/textColorPrimary" />
<LinearLayout
android:id="@+id/button_panel"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:measureWithLargestChild="true"
android:minHeight="54dp"
android:orientation="horizontal"
android:padding="2dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/negative"
style="@style/Widget.Button.Text"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:layout_weight="1"
android:maxLines="2"
tools:text="Negative" />
<com.google.android.material.button.MaterialButton
android:id="@+id/neutral"
style="@style/Widget.Button.Text"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:layout_weight="1"
android:maxLines="2"
tools:text="Neutral" />
<com.google.android.material.button.MaterialButton
android:id="@+id/positive"
style="@style/Widget.Button.Text"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:layout_weight="1"
android:maxLines="2"
tools:text="Positive" />
</LinearLayout>
</LinearLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/margin_generic">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/settings_update_custom_msg"
app:hintEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/custom_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="com.topjohnwu.magisk.view.MagiskDialog.Data" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/dialog_base_outside_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_default="wrap"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_max="400dp"
app:layout_constraintWidth_percent="1">
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card.Elevated"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="@dimen/margin_generic"
app:cardUseCompatPadding="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/dialog_base_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/dialog_base_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<FrameLayout
android:id="@+id/dialog_base_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/dialog_base_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
style="@style/WidgetFoundation.Icon.Primary"
gone="@{data.icon == 0}"
srcCompat="@{data.icon}"
android:layout_gravity="center"
android:layout_marginTop="@dimen/l1"
android:padding="0dp"
tools:src="@drawable/ic_delete_md2" />
<androidx.appcompat.widget.AppCompatImageView
style="@style/WidgetFoundation.Image.Big"
gone="@{data.iconRaw == null}"
android:layout_gravity="center"
android:layout_marginTop="@dimen/l1"
android:src="@{data.iconRaw}"
tools:src="@drawable/ic_delete_md2" />
</FrameLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/dialog_base_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:gravity="center"
android:text="@{data.title}"
android:textAppearance="@style/AppearanceFoundation.Title"
app:layout_constraintEnd_toEndOf="@+id/dialog_base_end"
app:layout_constraintStart_toStartOf="@+id/dialog_base_start"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_icon"
tools:lines="1"
tools:text="@tools:sample/lorem/random" />
<FrameLayout
android:id="@+id/dialog_base_scroll"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l_50"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@+id/dialog_base_space"
app:layout_constraintEnd_toEndOf="@+id/dialog_base_end"
app:layout_constraintStart_toStartOf="@+id/dialog_base_start"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_title">
<androidx.core.widget.NestedScrollView
gone="@{data.message.length == 0}"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/dialog_base_message"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@{data.message}"
android:textAppearance="@style/AppearanceFoundation.Body"
tools:lines="3"
tools:text="@tools:sample/lorem/random" />
</androidx.core.widget.NestedScrollView>
<FrameLayout
android:id="@+id/dialog_base_container"
gone="@{data.message.length != 0}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
<Space
android:id="@+id/dialog_base_space"
android:layout_width="wrap_content"
android:layout_height="@dimen/l1"
app:layout_constraintBottom_toTopOf="@+id/dialog_base_buttons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<FrameLayout
android:id="@+id/dialog_base_buttons"
style="?attr/buttonBarStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<androidx.appcompat.widget.ButtonBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom|center_horizontal"
android:layoutDirection="locale"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingTop="4dp"
android:paddingEnd="12dp"
android:paddingBottom="4dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/dialog_base_button_4"
style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonIDGAF.icon == 0 &amp;&amp; data.buttonIDGAF.title.length == 0}"
isEnabled="@{data.buttonIDGAF.isEnabled()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:filterTouchesWhenObscured="true"
android:onClick="@{() -> data.buttonIDGAF.clicked()}"
android:text="@{data.buttonIDGAF.title}"
app:icon="@{data.buttonIDGAF.icon}"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/dialog_base_button_2"
style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonNeutral.icon == 0 &amp;&amp; data.buttonNeutral.title.length == 0}"
isEnabled="@{data.buttonNeutral.isEnabled()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:filterTouchesWhenObscured="true"
android:onClick="@{() -> data.buttonNeutral.clicked()}"
android:text="@{data.buttonNeutral.title}"
app:icon="@{data.buttonNeutral.icon}"
app:iconGravity="textStart"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
<Space
android:id="@+id/spacer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="invisible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/dialog_base_button_3"
style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonNegative.icon == 0 &amp;&amp; data.buttonNegative.title.length == 0}"
isEnabled="@{data.buttonNegative.isEnabled()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:filterTouchesWhenObscured="true"
android:onClick="@{() -> data.buttonNegative.clicked()}"
android:text="@{data.buttonNegative.title}"
app:icon="@{data.buttonNegative.icon}"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/dialog_base_button_1"
style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonPositive.icon == 0 &amp;&amp; data.buttonPositive.title.length == 0}"
isEnabled="@{data.buttonPositive.isEnabled()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:filterTouchesWhenObscured="true"
android:onClick="@{() -> data.buttonPositive.clicked()}"
android:text="@{data.buttonPositive.title}"
app:icon="@{data.buttonPositive.icon}"
app:iconGravity="textStart"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
</androidx.appcompat.widget.ButtonBarLayout>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
......@@ -5,42 +5,48 @@
<data>
<import type="com.topjohnwu.magisk.extensions.XStringKt" />
<variable
name="data"
type="com.topjohnwu.magisk.ui.settings.SettingsFragment.ManagerNameData" />
type="com.topjohnwu.magisk.ui.settings.Hide" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="@dimen/margin_generic"
android:paddingTop="@dimen/margin_generic"
android:paddingEnd="@dimen/margin_generic">
android:padding="@dimen/margin_generic">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/dialog_name_input"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic"
android:hint="@string/settings_app_name_hint"
app:boxStrokeColor="?colorOnSurfaceVariant"
app:counterEnabled="true"
app:counterMaxLength="14"
app:counterOverflowTextColor="@color/colorError"
app:error="@{data.name.length() > 14 || data.name.empty || XStringKt.isEmptyInternal(data.name) ? @string/settings_app_name_error : @string/empty}"
app:error="@{data.error ? @string/settings_app_name_error : @string/empty}"
app:errorEnabled="true"
app:helperText="@string/settings_app_name_helper">
app:errorTextColor="?colorError"
app:helperText="@string/settings_app_name_helper"
app:hintEnabled="true"
app:hintTextAppearance="@style/AppearanceFoundation.Tiny"
app:hintTextColor="?colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_custom_download_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={data.name}"
tools:text="@string/re_app_name" />
android:inputType="textCapWords"
android:text="@={data.value}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textColor="?colorOnSurface"
tools:text="@tools:sample/lorem" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</layout>
\ No newline at end of file
</layout>
......@@ -7,7 +7,7 @@
<variable
name="data"
type="com.topjohnwu.magisk.ui.settings.SettingsFragment.DownloadDialogData" />
type="com.topjohnwu.magisk.ui.settings.DownloadPath" />
</data>
......@@ -22,6 +22,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{@string/settings_download_path_message(data.path)}"
android:textAppearance="@style/AppearanceFoundation.Caption"
tools:text="@string/settings_download_path_message" />
<com.google.android.material.textfield.TextInputLayout
......@@ -30,14 +31,21 @@
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic"
android:hint="@string/settings_download_path_title"
app:hintEnabled="true">
app:boxStrokeColor="?colorOnSurfaceVariant"
app:errorTextColor="?colorError"
app:hintEnabled="true"
app:hintTextAppearance="@style/AppearanceFoundation.Tiny"
app:hintTextColor="?colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_custom_download_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:text="@={data.text}" />
android:text="@={data.result}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textColor="?colorOnSurface"
tools:text="@tools:sample/lorem" />
</com.google.android.material.textfield.TextInputLayout>
......
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="com.topjohnwu.magisk.ui.settings.UpdateChannelUrl" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/margin_generic">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic"
android:hint="@string/settings_update_custom_msg"
app:boxStrokeColor="?colorOnSurfaceVariant"
app:errorTextColor="?colorError"
app:hintEnabled="true"
app:hintTextAppearance="@style/AppearanceFoundation.Tiny"
app:hintTextColor="?colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_custom_download_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:text="@={data.result}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textColor="?colorOnSurface"
tools:text="@tools:sample/lorem" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.flash.FlashViewModel" />
</data>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.core.widget.NestedScrollView>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.hide.HideViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/hide_content"
dividerVertical="@{R.drawable.divider_l1}"
invisibleUnless="@{viewModel.loaded || !viewModel.items.empty}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@{viewModel.insets.bottom}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_hide_md2"
tools:paddingTop="40dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/hide_filter_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
app:backgroundTint="?colorSurfaceSurfaceVariant"
app:srcCompat="@drawable/ic_filter"
app:tint="?colorPrimary"
tools:layout_marginBottom="64dp" />
<com.google.android.material.circularreveal.cardview.CircularRevealCardView
android:id="@+id/hide_filter"
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:visibility="invisible"
app:cardCornerRadius="0dp">
<include
android:id="@+id/hide_filter_include"
layout="@layout/include_hide_filter"
viewModel="@{viewModel}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.circularreveal.cardview.CircularRevealCardView>
<LinearLayout
goneUnless="@{viewModel.loading &amp;&amp; viewModel.items.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/safetynet_attest_loading"
android:textAppearance="@style/AppearanceFoundation.Title"
android:textStyle="bold" />
<ProgressBar
style="@style/WidgetFoundation.ProgressBar.Indeterminate"
android:layout_marginTop="@dimen/l1" />
</LinearLayout>
</FrameLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.os.Build" />
<import type="com.topjohnwu.magisk.R" />
<import type="com.topjohnwu.magisk.core.Info" />
<import type="com.topjohnwu.magisk.core.InfoKt" />
<import type="com.topjohnwu.magisk.core.Config" />
<import type="com.topjohnwu.magisk.BuildConfig" />
<import type="com.topjohnwu.magisk.ui.home.MagiskState" />
<import type="com.topjohnwu.magisk.extensions.XAndroidKt" />
<import type="com.topjohnwu.magisk.extensions.XSUKt" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.home.HomeViewModel" />
</data>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l2}"
tools:layout_marginTop="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="@dimen/l1">
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card.Primary"
goneUnless="@{viewModel.noticeVisible}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l1">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_notice_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="@dimen/l1"
android:text="@string/home_notice_content"
android:textAppearance="@style/AppearanceFoundation.Caption.OnPrimary"
app:layout_constraintEnd_toStartOf="@+id/home_notice_hide"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/home_notice_hide"
style="@style/WidgetFoundation.Button.Text.OnPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.hideNotice()}"
android:text="@string/home_notice_hide"
android:textAllCaps="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false">
<com.google.android.material.card.MaterialCardView
android:id="@+id/home_device_wrapper"
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_card"
android:orientation="horizontal">
<View
gone="@{XAndroidKt.isDeviceSecure()}"
android:layout_width="0dp"
android:layout_height="0dp"
android:alpha=".15"
android:background="?colorError"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/home_device_overview"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/home_device_icon"
style="@style/WidgetFoundation.Icon.Primary"
android:layout_marginStart="@dimen/l_50"
android:layout_marginTop="@dimen/l1"
android:layout_marginBottom="@dimen/l1"
android:background="@null"
android:padding="@dimen/l_50"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_device"
app:tint="@color/color_primary_error_transient" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_device_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:gravity="start|center_vertical"
android:text="@{Build.MANUFACTURER}"
android:textAppearance="@style/AppearanceFoundation.Title"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/home_device_product"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/home_device_icon"
app:layout_constraintTop_toTopOf="@+id/home_device_icon"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Google" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_device_product"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{Build.DEVICE ?? Build.PRODUCT}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintBottom_toTopOf="@+id/home_device_board"
app:layout_constraintEnd_toEndOf="@+id/home_device_title"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@+id/home_device_title"
app:layout_constraintTop_toBottomOf="@+id/home_device_title"
tools:text="Pixel 4" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_device_board"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{Build.BOARD}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintBottom_toBottomOf="@+id/home_device_icon"
app:layout_constraintEnd_toEndOf="@+id/home_device_title"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@+id/home_device_title"
app:layout_constraintTop_toBottomOf="@+id/home_device_product"
tools:text="flame" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/home_device_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/l1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_device_overview"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_device_extra_security_title"
style="@style/W.Home.Extra.Title"
android:text="@string/home_device_security"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_device_extra_security_value"
style="@style/W.Home.Extra.Value"
android:text="@{XAndroidKt.isDeviceSecure() ? @string/home_device_security_secure : @string/home_device_security_outdated}"
app:layout_constraintBottom_toBottomOf="@+id/home_device_extra_security_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_device_extra_security_title"
app:layout_constraintTop_toTopOf="@+id/home_device_extra_security_title"
tools:text="@string/home_device_security_secure" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_device_extra_version_title"
style="@style/W.Home.Extra.Title"
android:text="@string/home_device_system"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_device_extra_security_title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_device_extra_version_value"
style="@style/W.Home.Extra.Value"
android:text="@{Build.VERSION.RELEASE}"
app:layout_constraintBottom_toBottomOf="@+id/home_device_extra_version_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_device_extra_version_title"
app:layout_constraintTop_toTopOf="@+id/home_device_extra_version_title"
tools:text="10" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_device_extra_build_style_title"
style="@style/W.Home.Extra.Title"
android:text="@string/home_device_build_style"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_device_extra_version_title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_device_extra_build_style_value"
style="@style/W.Home.Extra.Value"
android:text="@{XAndroidKt.isAB ? @string/yes : @string/no}"
app:layout_constraintBottom_toBottomOf="@+id/home_device_extra_build_style_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_device_extra_build_style_title"
app:layout_constraintTop_toTopOf="@+id/home_device_extra_build_style_title"
tools:text="Yes" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_device_extra_build_type_title"
style="@style/W.Home.Extra.Title"
android:text="@string/home_device_build_type"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_device_extra_build_style_title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_device_extra_build_type_value"
style="@style/W.Home.Extra.Value"
android:text="@{XAndroidKt.isSAR ? @string/yes : @string/no}"
app:layout_constraintBottom_toBottomOf="@+id/home_device_extra_build_type_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_device_extra_build_type_title"
app:layout_constraintTop_toTopOf="@+id/home_device_extra_build_type_title"
tools:text="Yes" />
<!--todo(topjohnwu) Add device info here-->
<!--Copy & paste "..._title" and "..._value"-->
<!--Change ids to match "home_device_extra_*info-type*_value" or "..._title"-->
<!--Fix constraints by replacing ids (views are always bound to "title")-->
<androidx.constraintlayout.widget.Group
goneUnless="@{XSUKt.hasRoot}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="home_device_extra_build_style_title,home_device_extra_build_style_value,home_device_extra_build_type_title,home_device_extra_build_type_value"
tools:visibility="gone" />
<!--todo(topjohnwu) If info requires root add id to this group-->
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/home_magisk_wrapper"
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l_50"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@+id/home_magisk_buttons"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_device_wrapper"
app:layout_constraintWidth_max="400dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_card"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:paddingStart="@dimen/l_50"
android:paddingTop="@dimen/l1"
android:paddingBottom="@dimen/l1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/home_magisk_info"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/home_magisk_icon"
style="@style/WidgetFoundation.Icon.Primary"
android:background="@null"
android:padding="@dimen/l_25"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_magisk_delete" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_magisk_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l_50"
android:gravity="start|center_vertical"
android:text="@string/magisk"
android:textAppearance="@style/AppearanceFoundation.Title"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/home_magisk_status"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_magisk_icon"
app:layout_constraintTop_toTopOf="@+id/home_magisk_icon"
app:layout_constraintVertical_chainStyle="packed" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_magisk_status"
gone="@{viewModel.stateTextMagisk.empty}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{viewModel.stateTextMagisk}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/home_magisk_version"
app:layout_constraintEnd_toEndOf="@+id/home_magisk_title"
app:layout_constraintStart_toStartOf="@+id/home_magisk_title"
app:layout_constraintTop_toBottomOf="@+id/home_magisk_title"
tools:text="@string/up_to_date_md2" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_magisk_version"
gone="@{viewModel.stateVersionUpdateMagisk.empty}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{viewModel.stateVersionUpdateMagisk}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="@+id/home_magisk_icon"
app:layout_constraintEnd_toEndOf="@+id/home_magisk_title"
app:layout_constraintStart_toStartOf="@+id/home_magisk_title"
tools:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/home_magisk_status" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/home_magisk_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:padding="@dimen/l1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="150dp">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_magisk_extra_version_title"
style="@style/W.Home.Extra.Title"
android:text="@string/home_extra_version"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_magisk_extra_version_value"
style="@style/W.Home.Extra.Value"
android:text="@{Info.env.magiskVersionString}"
app:layout_constraintBottom_toBottomOf="@+id/home_magisk_extra_version_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_magisk_extra_version_title"
app:layout_constraintTop_toTopOf="@+id/home_magisk_extra_version_title"
tools:text="20.1" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_magisk_extra_code_title"
style="@style/W.Home.Extra.Title"
android:text="@string/home_extra_code"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_magisk_extra_version_title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_magisk_extra_code_value"
style="@style/W.Home.Extra.Value"
android:text="@{Integer.toString(Info.env.magiskVersionCode)}"
app:layout_constraintBottom_toBottomOf="@+id/home_magisk_extra_code_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_magisk_extra_code_title"
app:layout_constraintTop_toTopOf="@+id/home_magisk_extra_code_title"
tools:text="12345" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_magisk_extra_mode_title"
style="@style/W.Home.Extra.Title"
android:text="@string/home_extra_mode"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_magisk_extra_code_title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_magisk_extra_mode_value"
style="@style/W.Home.Extra.Value"
android:text="@{Config.coreOnly ? @string/home_extra_mode_safe : @string/home_extra_mode_normal}"
app:layout_constraintBottom_toBottomOf="@+id/home_magisk_extra_mode_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_magisk_extra_mode_title"
app:layout_constraintTop_toTopOf="@+id/home_magisk_extra_mode_title"
tools:text="Normal" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_magisk_extra_connection_title"
style="@style/W.Home.Extra.Title"
android:text="@string/home_extra_connection"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_magisk_extra_mode_title" />
<!--todo(topjohnwu) fix connection type-->
<!--don't forget to bind verbose values to proper resources-->
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_magisk_extra_connection_value"
style="@style/W.Home.Extra.Value"
app:layout_constraintBottom_toBottomOf="@+id/home_magisk_extra_connection_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_magisk_extra_connection_title"
app:layout_constraintTop_toTopOf="@+id/home_magisk_extra_connection_title"
tools:text="Normal" />
<androidx.constraintlayout.widget.Group
gone="@{Info.env.magiskVersionCode &lt; 0}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="home_magisk_extra_version_title,home_magisk_extra_version_value,home_magisk_extra_code_title,home_magisk_extra_code_value,home_magisk_extra_mode_title,home_magisk_extra_mode_value,home_magisk_extra_connection_title,home_magisk_extra_connection_value" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
style="@style/WidgetFoundation.ProgressBar"
gone="@{viewModel.stateMagiskProgress == 0 || viewModel.stateMagiskProgress == 100}"
android:layout_width="match_parent"
android:layout_gravity="bottom"
android:max="100"
android:progress="@{viewModel.stateMagiskProgress}" />
</com.google.android.material.card.MaterialCardView>
<FrameLayout
android:id="@+id/home_magisk_buttons"
gone="@{viewModel.stateMagisk == MagiskState.LOADING || !viewModel.isConnected}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/l_50"
android:animateLayoutChanges="true"
app:layout_constraintBottom_toBottomOf="@+id/home_magisk_wrapper"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/home_magisk_wrapper"
app:layout_constraintTop_toTopOf="@+id/home_magisk_wrapper"
app:layout_constraintVertical_bias=".5">
<com.google.android.material.floatingactionbutton.FloatingActionButton
gone="@{viewModel.stateMagisk != MagiskState.OBSOLETE}"
isEnabled="@{viewModel.stateManager == MagiskState.UP_TO_DATE &amp;&amp; (viewModel.stateMagiskProgress == 0 || viewModel.stateMagiskProgress == 100)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:onClick="@{() -> viewModel.onMagiskPressed()}"
android:text="@string/update"
app:backgroundTint="?colorPrimary"
app:elevation="0dp"
app:fabSize="mini"
app:srcCompat="@drawable/ic_update_md2"
app:tint="?colorOnPrimary"
tools:visibility="gone" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
gone="@{viewModel.stateMagisk == MagiskState.OBSOLETE}"
isEnabled="@{viewModel.stateManager == MagiskState.UP_TO_DATE &amp;&amp; (viewModel.stateMagiskProgress == 0 || viewModel.stateMagiskProgress == 100)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:onClick="@{() -> viewModel.onMagiskPressed()}"
android:text="@string/install"
app:backgroundTint="?colorSurface"
app:elevation="0dp"
app:fabSize="mini"
app:srcCompat="@drawable/ic_install"
app:tint="?colorPrimary" />
</FrameLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/home_manager_wrapper"
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l_50"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@+id/home_manager_buttons"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_magisk_wrapper"
app:layout_constraintWidth_max="400dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_card"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="@dimen/l_50"
android:paddingTop="@dimen/l1"
android:paddingBottom="@dimen/l1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/home_manager_info"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/home_manager_icon"
style="@style/WidgetFoundation.Icon.Primary"
android:background="@null"
android:padding="@dimen/l_50"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_manager" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_manager_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l_50"
android:gravity="start|center_vertical"
android:text="@{viewModel.stateHideManagerName}"
android:textAppearance="@style/AppearanceFoundation.Title"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/home_manager_status"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_manager_icon"
app:layout_constraintTop_toTopOf="@+id/home_manager_icon"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/manager" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_manager_status"
gone="@{viewModel.stateTextManager.empty}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{viewModel.stateTextManager}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/home_manager_version"
app:layout_constraintEnd_toEndOf="@+id/home_manager_title"
app:layout_constraintStart_toStartOf="@+id/home_manager_title"
app:layout_constraintTop_toBottomOf="@+id/home_manager_title"
tools:text="@string/obsolete_md2" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_manager_version"
gone="@{viewModel.stateVersionUpdateManager.empty}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{viewModel.stateVersionUpdateManager}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="@+id/home_manager_icon"
app:layout_constraintEnd_toEndOf="@+id/home_manager_title"
app:layout_constraintStart_toStartOf="@+id/home_manager_title"
app:layout_constraintTop_toBottomOf="@+id/home_manager_status"
tools:text="10.0.0 > 10.0.1" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/home_manager_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:padding="@dimen/l1"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="150dp">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_manager_extra_version_title"
style="@style/W.Home.Extra.Title"
android:text="@string/home_extra_version"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_manager_extra_version_value"
style="@style/W.Home.Extra.Value"
android:text="@{BuildConfig.VERSION_NAME}"
app:layout_constraintBottom_toBottomOf="@+id/home_manager_extra_version_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_manager_extra_version_title"
app:layout_constraintTop_toTopOf="@+id/home_manager_extra_version_title"
tools:text="8.0.0" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_manager_extra_code_title"
style="@style/W.Home.Extra.Title"
android:text="@string/home_extra_code"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_manager_extra_version_title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_manager_extra_code_value"
style="@style/W.Home.Extra.Value"
android:text="@{Integer.toString(BuildConfig.VERSION_CODE)}"
app:layout_constraintBottom_toBottomOf="@+id/home_manager_extra_code_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_manager_extra_code_title"
app:layout_constraintTop_toTopOf="@+id/home_manager_extra_code_title"
tools:text="12345" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_manager_extra_mode_title"
style="@style/W.Home.Extra.Title"
android:text="@string/home_extra_mode"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_manager_extra_code_title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_manager_extra_mode_value"
style="@style/W.Home.Extra.Value"
android:text="@{InfoKt.isRunningAsStub ? @string/home_extra_mode_stub : @string/home_extra_mode_normal}"
app:layout_constraintBottom_toBottomOf="@+id/home_manager_extra_mode_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_manager_extra_mode_title"
app:layout_constraintTop_toTopOf="@+id/home_manager_extra_mode_title"
tools:text="@string/home_extra_mode_stub" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_manager_extra_package_title"
style="@style/W.Home.Extra.Title"
android:text="@string/home_extra_package"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_manager_extra_mode_title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/home_manager_extra_package_value"
style="@style/W.Home.Extra.Value"
android:text="@{viewModel.statePackageManager}"
app:layout_constraintBottom_toBottomOf="@+id/home_manager_extra_package_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_manager_extra_package_title"
app:layout_constraintTop_toTopOf="@+id/home_manager_extra_package_title"
tools:text="com.soqspa.xmclaeixl" />
<androidx.constraintlayout.widget.Group
gone="@{viewModel.statePackageOriginal}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="home_manager_extra_package_title,home_manager_extra_package_value" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
style="@style/WidgetFoundation.ProgressBar"
gone="@{viewModel.stateManagerProgress == 0 || viewModel.stateManagerProgress == 100}"
android:layout_width="match_parent"
android:layout_gravity="bottom"
android:max="100"
android:progress="@{viewModel.stateManagerProgress}" />
</com.google.android.material.card.MaterialCardView>
<FrameLayout
android:id="@+id/home_manager_buttons"
gone="@{viewModel.stateManager == MagiskState.LOADING || !viewModel.isConnected}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/l_50"
app:layout_constraintBottom_toBottomOf="@+id/home_manager_wrapper"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/home_manager_wrapper"
app:layout_constraintTop_toTopOf="@+id/home_manager_wrapper"
app:layout_constraintVertical_bias=".5">
<com.google.android.material.floatingactionbutton.FloatingActionButton
gone="@{viewModel.stateManager != MagiskState.OBSOLETE}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:onClick="@{() -> viewModel.onManagerPressed()}"
app:backgroundTint="?colorPrimary"
app:elevation="0dp"
app:fabSize="mini"
app:srcCompat="@drawable/ic_update_md2"
app:tint="?colorOnPrimary" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
gone="@{viewModel.stateManager == MagiskState.OBSOLETE}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:onClick="@{() -> viewModel.onManagerPressed()}"
app:backgroundTint="?colorSurface"
app:elevation="0dp"
app:fabSize="mini"
app:srcCompat="@drawable/ic_install"
app:tint="?colorPrimary"
tools:visibility="gone" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_margin="@dimen/l1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/l1">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:text="@string/home_support_title"
android:textAppearance="@style/AppearanceFoundation.Title" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:text="@string/home_support_content"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant" />
<androidx.recyclerview.widget.RecyclerView
dividerHorizontal="@{R.drawable.divider_l1}"
dividerVertical="@{R.drawable.divider_l1}"
itemBinding="@{viewModel.itemDeveloperBinding}"
items="@{viewModel.items}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:clipToPadding="false"
android:nestedScrollingEnabled="@{false}"
android:orientation="horizontal"
android:overScrollMode="ifContentScrolls"
android:paddingStart="@dimen/l1"
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
app:spanCount="2"
tools:itemCount="3"
tools:listitem="@layout/item_developer" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton
style="@style/WidgetFoundation.Button.Error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:onClick="@{() -> viewModel.onDeletePressed()}"
android:text="@string/uninstall"
android:textAllCaps="false"
app:icon="@drawable/ic_delete_md2" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.install.InstallViewModel" />
</data>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l2}"
tools:paddingTop="24dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
goneUnless="@{viewModel.loaded}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="@dimen/l1">
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
app:strokeWidth="@{viewModel.step != 0 ? 0f : @dimen/l_125}">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?listPreferredItemHeightSmall">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/install_step_options_icon"
style="@style/WidgetFoundation.Icon"
isSelected="@{viewModel.step > 0}"
android:layout_marginStart="@dimen/l_25"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_check_circle_md2" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/install_step_options_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:gravity="start"
android:text="@string/install_options_title"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/install_step_options_button"
app:layout_constraintStart_toEndOf="@+id/install_step_options_icon"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/install_step_options_button"
style="@style/WidgetFoundation.Button.Text"
gone="@{viewModel.step != 0}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.step(1)}"
android:text="@string/install_next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
gone="@{viewModel.step != 0}"
android:layout_width="match_parent"
android:paddingStart="3dp"
android:paddingEnd="3dp"
android:layout_height="wrap_content">
<include
layout="@layout/include_install_options"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50" />
</FrameLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
app:strokeWidth="@{viewModel.step != 1 ? 0f : @dimen/l_125}">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?listPreferredItemHeightSmall">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/install_step_method_icon"
style="@style/WidgetFoundation.Icon"
isSelected="@{viewModel.step > 1}"
android:layout_marginStart="@dimen/l_25"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_check_circle_md2" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/install_step_method_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:gravity="start"
android:text="@string/install_method_title"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/install_step_method_button"
app:layout_constraintStart_toEndOf="@+id/install_step_method_icon"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/install_step_method_button"
style="@style/WidgetFoundation.Button.Text"
gone="@{viewModel.step != 1}"
isEnabled="@{viewModel.method == @id/method_patch ? viewModel.data != null : viewModel.method != -1}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.step(2)}"
android:text="@string/install_next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<RadioGroup
gone="@{viewModel.step != 1}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="3dp"
android:paddingEnd="3dp"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
android:checkedButton="@={viewModel.method}">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/method_download"
style="@style/WidgetFoundation.RadioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/download_zip_only"
tools:checked="true" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/method_patch"
style="@style/WidgetFoundation.RadioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonCompat="@drawable/ic_radio_check_button"
android:text="@string/select_patch_file" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/method_direct"
style="@style/WidgetFoundation.RadioButton"
gone="@{!viewModel.rooted}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonCompat="@drawable/ic_radio_check_button"
android:text="@string/direct_install" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/method_inactive_slot"
style="@style/WidgetFoundation.RadioButton"
gone="@{!viewModel.rooted || !viewModel.aB}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonCompat="@drawable/ic_radio_check_button"
android:text="@string/install_inactive_slot" />
</RadioGroup>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton
style="@style/WidgetFoundation.Button.Text"
gone="@{viewModel.step != 2}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_margin="@dimen/l1"
android:onClick="@{() -> viewModel.install()}"
android:text="@string/install_start"
app:icon="@drawable/ic_forth_md2"
app:iconGravity="textEnd" />
</LinearLayout>
<LinearLayout
goneUnless="@{viewModel.loading}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:gravity="center"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/safetynet_attest_loading"
android:textAppearance="@style/AppearanceFoundation.Title" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1">
<ProgressBar
style="@style/WidgetFoundation.ProgressBar"
invisible="@{viewModel.progress &lt;= 0}"
android:layout_width="100dp"
android:layout_gravity="center"
android:progress="@{viewModel.progress}" />
<ProgressBar
style="@style/WidgetFoundation.ProgressBar.Indeterminate"
invisibleUnless="@{viewModel.progress &lt;= 0}"
android:layout_width="100dp"
android:layout_gravity="center" />
</FrameLayout>
</LinearLayout>
</FrameLayout>
</androidx.core.widget.NestedScrollView>
</layout>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.log.LogViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/log_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:elevation="4dp"
app:tabSelectedTextColor="@android:color/white"
app:tabTextColor="@android:color/secondary_text_dark" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/log_container"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
pageTitles="@{viewModel}"
position="@={viewModel.currentPage}"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.log.LogViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@{viewModel.insets.bottom}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_log_access_md2"
tools:paddingTop="24dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/log_filter_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
app:backgroundTint="?colorSurfaceSurfaceVariant"
app:srcCompat="@drawable/ic_folder_list"
app:tint="?colorPrimary"
tools:layout_marginBottom="64dp" />
<com.google.android.material.circularreveal.cardview.CircularRevealCardView
android:id="@+id/log_filter"
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:visibility="invisible"
app:cardCornerRadius="0dp">
<include
android:id="@+id/log_filter_include"
layout="@layout/include_log_magisk"
viewModel="@{viewModel}"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.google.android.material.circularreveal.cardview.CircularRevealCardView>
<FrameLayout
gone="@{!viewModel.items.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center">
<include
item="@{viewModel.itemEmpty}"
layout="@layout/item_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
</FrameLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State" />
<import type="com.topjohnwu.magisk.ui.home.SafetyNetState" />
<import type="com.topjohnwu.magisk.ui.home.MagiskState" />
<import type="com.topjohnwu.magisk.ui.home.MagiskItem" />
<import type="com.topjohnwu.magisk.R" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.home.HomeViewModel" />
</data>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
app:onRefreshListener="@{() -> viewModel.refresh()}"
app:refreshing="@{viewModel.loading}">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="8dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/margin_generic">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="@+id/app_name"
app:layout_constraintEnd_toStartOf="@+id/app_name"
app:layout_constraintHorizontal_bias="0.42"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/app_name"
app:srcCompat="@drawable/ic_logo" />
<TextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
style="@style/Widget.Divider.Horizontal"
android:layout_width="match_parent"
android:layout_margin="@dimen/margin_generic" />
<include
android:id="@+id/home_magisk_version"
additionalInfo="@{viewModel.magiskAdditionalInfo}"
currentVersion="@{viewModel.magiskCurrentVersion}"
item="@{MagiskItem.MAGISK}"
latestVersion="@{viewModel.magiskLatestVersion}"
layout="@layout/include_update_card"
state="@{viewModel.magiskState}"
text="@{viewModel.magiskStateText}"
viewModel="@{viewModel}" />
<include
android:id="@+id/home_manager_version"
additionalInfo="@{viewModel.managerAdditionalInfo}"
currentVersion="@{viewModel.managerCurrentVersion}"
item="@{MagiskItem.MANAGER}"
latestVersion="@{viewModel.managerLatestVersion}"
layout="@layout/include_update_card"
state="@{viewModel.managerState}"
text="@{viewModel.managerStateText}"
viewModel="@{viewModel}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic" />
<View
style="@style/Widget.Divider.Horizontal"
gone="@{!viewModel.isConnected}"
android:layout_width="match_parent"
android:layout_margin="@dimen/margin_generic" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
gone="@{!viewModel.isConnected}"
android:onClick="@{() -> viewModel.advancedPressed()}"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:minHeight="48dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:rotation="@{viewModel.isAdvancedExpanded ? 180 : 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/title"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow"
app:tint="?attr/imageColorTint" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/advanced_settings_title"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/install_option_expand"
gone="@{!viewModel.isAdvancedExpanded}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<CheckBox
android:id="@+id/keep_force_enc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:checked="@={viewModel.isForceEncryption}"
android:text="@string/keep_force_encryption"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap"
app:layout_constraintWidth_min="300dp" />
<CheckBox
android:id="@+id/keep_verity"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:checked="@={viewModel.isKeepVerity}"
android:text="@string/keep_dm_verity"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keep_force_enc"
app:layout_constraintWidth_default="wrap"
app:layout_constraintWidth_min="300dp" />
<CheckBox
android:id="@+id/recovery_mode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:checked="@={viewModel.isRecovery}"
android:text="@string/recovery_mode"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keep_verity"
app:layout_constraintWidth_default="wrap"
app:layout_constraintWidth_min="300dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<View
style="@style/Widget.Divider.Horizontal"
gone="@{!viewModel.isActive || !viewModel.isConnected}"
android:layout_width="match_parent"
android:layout_margin="@dimen/margin_generic" />
<LinearLayout
gone="@{!viewModel.isConnected || !viewModel.hasGMS}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/margin_generic_half"
android:paddingBottom="@dimen/margin_generic_half">
<ImageView
android:id="@+id/sn_logo"
android:layout_width="30dp"
android:layout_height="30dp"
android:tint="@color/green500"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/safetyNet_status"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_safetynet" />
<TextView
android:id="@+id/safetyNet_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_generic"
android:layout_marginRight="@dimen/margin_generic"
android:gravity="center"
android:maxLines="1"
android:text="@{viewModel.safetyNetTitle}"
android:textStyle="bold"
app:autoSizeMaxTextSize="14sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/safetyNet_refresh"
app:layout_constraintStart_toEndOf="@+id/sn_logo"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="200dp" />
<ImageView
android:id="@+id/safetyNet_refresh"
invisible="@{viewModel.ctsState == SafetyNetState.LOADING || viewModel.basicIntegrityState == SafetyNetState.LOADING}"
android:layout_width="25dp"
android:layout_height="25dp"
android:onClick="@{() -> viewModel.safetyNetPressed()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/safetyNet_status"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_refresh" />
<ProgressBar
android:id="@+id/safetyNet_check_progress"
style="@style/Widget.Progress"
goneUnless="@{viewModel.ctsState == SafetyNetState.LOADING || viewModel.basicIntegrityState == SafetyNetState.LOADING}"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/safetyNet_refresh"
app:layout_constraintEnd_toEndOf="@+id/safetyNet_refresh"
app:layout_constraintStart_toStartOf="@+id/safetyNet_refresh"
app:layout_constraintTop_toTopOf="@+id/safetyNet_refresh" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/safetyNet_expand"
gone="@{viewModel.safetyNetState == State.LOADING}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/cts_status_icon"
srcCompat="@{viewModel.ctsState == SafetyNetState.PASS ? R.drawable.ic_check_circle : R.drawable.ic_cancel}"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="@+id/cts_status"
app:layout_constraintEnd_toStartOf="@+id/cts_status"
app:layout_constraintTop_toTopOf="@+id/cts_status"
app:tint="@{viewModel.ctsState == SafetyNetState.PASS ? @color/colorCorrect : @color/colorError}"
tools:srcCompat="@drawable/ic_check_circle" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/basic_status_icon"
srcCompat="@{viewModel.basicIntegrityState == SafetyNetState.PASS ? R.drawable.ic_check_circle : R.drawable.ic_cancel}"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="@+id/basic_status"
app:layout_constraintEnd_toStartOf="@+id/basic_status"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/basic_status"
app:tint="@{viewModel.basicIntegrityState == SafetyNetState.PASS ? @color/colorCorrect : @color/colorError}"
tools:srcCompat="@drawable/ic_check_circle" />
<TextView
android:id="@+id/cts_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@{String.format(`ctsProfile: %b`, viewModel.ctsState == SafetyNetState.PASS)}"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/basic_status"
app:layout_constraintStart_toStartOf="@+id/basic_status"
app:layout_constraintTop_toTopOf="parent"
tools:text="ctsProfile: true" />
<TextView
android:id="@+id/basic_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:padding="6dp"
android:text="@{String.format(`basicIntegrity: %b`, viewModel.basicIntegrityState == SafetyNetState.PASS)}"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/basic_status_icon"
app:layout_constraintTop_toBottomOf="@+id/cts_status"
tools:text="basicIntegrity: true" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<View
style="@style/Widget.Divider.Horizontal"
android:layout_width="match_parent"
android:layout_margin="@dimen/margin_generic" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/twitter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:onClick="@{() -> viewModel.twitterPressed()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/github"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/paypal">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center_horizontal"
android:layout_margin="10dp"
android:tint="?attr/imageColorTint"
app:srcCompat="@drawable/ic_twitter" />
</FrameLayout>
<FrameLayout
android:id="@+id/github"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:onClick="@{() -> viewModel.githubPressed()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/xda"
app:layout_constraintStart_toEndOf="@+id/twitter">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center_horizontal"
android:layout_margin="10dp"
android:tint="?attr/imageColorTint"
app:srcCompat="@drawable/ic_github" />
</FrameLayout>
<FrameLayout
android:id="@+id/xda"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:onClick="@{() -> viewModel.xdaPressed()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/github">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center_horizontal"
android:layout_margin="10dp"
android:tint="?attr/imageColorTint"
app:srcCompat="@drawable/ic_xda" />
</FrameLayout>
<LinearLayout
android:id="@+id/patreon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_horizontal"
android:onClick="@{() -> viewModel.patreonPressed()}"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/paypal"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:layout_margin="10dp"
android:tint="?attr/imageColorTint"
app:srcCompat="@drawable/ic_patreon" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:text="Patreon"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/paypal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_horizontal"
android:onClick="@{() -> viewModel.paypalPressed()}"
android:orientation="horizontal"
app:layout_constraintEnd_toStartOf="@+id/patreon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:layout_margin="10dp"
android:tint="?attr/imageColorTint"
app:srcCompat="@drawable/ic_paypal" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:text="PayPal"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<View
style="@style/Widget.Divider.Horizontal"
gone="@{!viewModel.isActive}"
android:layout_width="match_parent"
android:layout_margin="@dimen/margin_generic" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.Button.Text"
gone="@{!viewModel.isActive}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.uninstallPressed()}"
android:text="@string/uninstall" />
</LinearLayout>
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.hide.HideViewModel" />
</data>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:onRefreshListener="@{() -> viewModel.refresh()}"
app:refreshing="@{viewModel.loading}">
<androidx.recyclerview.widget.RecyclerView
dividerColor="@{@android:color/transparent}"
dividerSize="@{@dimen/margin_generic}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/margin_generic"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_hide_app" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<import type="com.topjohnwu.magisk.core.Config" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/module_list"
adapter="@{viewModel.adapter}"
dividerHorizontal="@{R.drawable.divider_l1}"
dividerVertical="@{R.drawable.divider_l1}"
gone="@{viewModel.loading &amp;&amp; viewModel.items.empty}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingEnd="0dp"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
app:spanCount="2"
tools:listitem="@layout/item_module_md2" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/module_filter_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
app:backgroundTint="?colorSurfaceSurfaceVariant"
app:srcCompat="@drawable/ic_filter"
app:tint="?colorPrimary"
tools:layout_marginBottom="64dp" />
<com.google.android.material.circularreveal.cardview.CircularRevealCardView
android:id="@+id/module_filter"
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:visibility="invisible"
app:cardCornerRadius="0dp">
<include
android:id="@+id/module_filter_include"
layout="@layout/include_module_filter"
viewModel="@{viewModel}"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.google.android.material.circularreveal.cardview.CircularRevealCardView>
<LinearLayout
goneUnless="@{viewModel.loading &amp;&amp; viewModel.items.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/safetynet_attest_loading"
android:textAppearance="@style/AppearanceFoundation.Title"
android:textStyle="bold" />
<ProgressBar
style="@style/WidgetFoundation.ProgressBar.Indeterminate"
android:layout_marginTop="@dimen/l1" />
</LinearLayout>
</FrameLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/modules_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:onRefreshListener="@{() -> viewModel.refresh(false)}"
app:refreshing="@{viewModel.loading}">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/modules_content"
dividerColor="@{@android:color/transparent}"
dividerSize="@{@dimen/margin_generic}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.itemsInstalled}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/margin_generic"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_module" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/modules_status_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/no_modules_found"
android:textSize="20sp"
android:textStyle="italic"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_margin="@dimen/fab_padding"
android:onClick="@{() -> viewModel.fabPressed()}"
android:focusable="true"
android:clickable="true"
app:fabSize="normal"
app:layout_behavior="com.google.android.material.floatingactionbutton.FloatingActionButton$Behavior"
app:srcCompat="@drawable/ic_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/repos_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:onRefreshListener="@{() -> viewModel.refresh(true)}"
app:refreshing="@{viewModel.loading}">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/repos_content"
dividerColor="@{@android:color/transparent}"
dividerSize="@{@dimen/margin_generic}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.itemsRemote}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/margin_generic"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_repo" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/repos_status_text"
gone="@{!(viewModel.loaded &amp;&amp; viewModel.itemsRemote.size == 0)}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/no_modules_found"
android:textSize="20sp"
android:textStyle="italic" />
</FrameLayout>
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.os.Build" />
<import type="com.topjohnwu.magisk.core.Info" />
<import type="com.topjohnwu.magisk.BuildConfig" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.safetynet.SafetynetViewModel" />
</data>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:paddingBottom="@{viewModel.insets.bottom}"
tools:paddingBottom="48dp"
tools:paddingTop="24dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
invisibleUnless="@{viewModel.checking}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/safetynet_attest_loading"
android:textAppearance="@style/AppearanceFoundation.Title" />
<androidx.constraintlayout.widget.ConstraintLayout
invisibleUnless="@{viewModel.success || viewModel.failed}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/l1"
android:paddingBottom="@dimen/l1">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/safetynet_icon"
style="@style/WidgetFoundation.Image.Big"
app:layout_constraintBottom_toTopOf="@+id/safetynet_magisk_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias=".3"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="@drawable/ic_magisk"
app:tint="?colorOnSurface" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/safetynet_magisk_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:text="@string/magisk"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintBottom_toTopOf="@+id/safetynet_manager_title"
app:layout_constraintEnd_toStartOf="@+id/safetynet_magisk_version"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/safetynet_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/safetynet_magisk_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l_50"
android:text="@{Info.env.magiskVersionString}"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/safetynet_magisk_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/safetynet_magisk_title"
app:layout_constraintTop_toTopOf="@+id/safetynet_magisk_title"
tools:text="20.0" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/safetynet_manager_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/manager"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintBottom_toTopOf="@+id/safetynet_android_title"
app:layout_constraintEnd_toStartOf="@+id/safetynet_manager_version"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/safetynet_magisk_title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/safetynet_manager_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l_50"
android:text="@{BuildConfig.VERSION_NAME}"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/safetynet_manager_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/safetynet_manager_title"
app:layout_constraintTop_toTopOf="@+id/safetynet_manager_title"
tools:text="10.0.0" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/safetynet_android_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/system"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintBottom_toTopOf="@+id/safetynet_attestation"
app:layout_constraintEnd_toStartOf="@+id/safetynet_android_version"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/safetynet_manager_title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/safetynet_android_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l_50"
android:text="@{Build.VERSION.RELEASE}"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/safetynet_android_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/safetynet_android_title"
app:layout_constraintTop_toTopOf="@+id/safetynet_android_title"
tools:text="10" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/safetynet_attestation"
style="@style/WidgetFoundation.Card.Elevated"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l2"
android:layout_marginEnd="@dimen/l1"
app:cardCornerRadius="@dimen/l1"
app:cardElevation="@dimen/l1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/safetynet_android_title"
app:layout_constraintWidth_max="300dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
goneUnless="@{viewModel.failed}"
android:layout_width="match_parent"
android:layout_height="0dp"
android:alpha=".5"
android:background="?colorError"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
goneUnless="@{viewModel.success}"
android:layout_width="match_parent"
android:layout_height="0dp"
android:alpha=".5"
android:background="?colorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/safetynet_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l2"
android:layout_marginEnd="@dimen/l1"
android:gravity="center"
android:text="@{viewModel.safetyNetTitle}"
android:textAppearance="@style/AppearanceFoundation.Display.OnPrimary"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/safetynet_attest_success" />
<View
android:id="@+id/safetynet_divider"
android:layout_width="50dp"
android:layout_height="4dp"
android:layout_marginTop="@dimen/l2"
android:background="@drawable/bg_divider_rounded_on_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/safetynet_title" />
<LinearLayout
android:id="@+id/safetynet_cts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/safetynet_divider">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:text="ctsProfile"
android:textAppearance="@style/AppearanceFoundation.Body.OnPrimary"
android:textStyle="bold"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatImageView
style="@style/WidgetFoundation.Icon.OnPrimary"
isSelected="@{viewModel.ctsState}"
android:layout_gravity="center"
app:srcCompat="@drawable/ic_check_circle_md2" />
</LinearLayout>
<LinearLayout
android:id="@+id/safetynet_basicintegrity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/l1"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/safetynet_cts">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:text="basicIntegrity"
android:textAppearance="@style/AppearanceFoundation.Body.OnPrimary"
android:textStyle="bold"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatImageView
style="@style/WidgetFoundation.Icon.OnPrimary"
isSelected="@{viewModel.basicIntegrityState}"
android:layout_gravity="center"
app:srcCompat="@drawable/ic_check_circle_md2" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton
style="@style/WidgetFoundation.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:layout_marginBottom="@dimen/l1"
android:onClick="@{() -> viewModel.reset()}"
android:text="@string/safetynet_attest_restart"
android:textAllCaps="false"
android:textColor="?colorOnSurfaceVariant"
app:icon="@drawable/ic_refresh_safetynet_md2"
app:iconTint="?colorOnSurfaceVariant"
app:layout_constraintEnd_toEndOf="@+id/safetynet_attestation"
app:layout_constraintTop_toBottomOf="@+id/safetynet_attestation" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</androidx.core.widget.NestedScrollView>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.settings.SettingsViewModel" />
</data>
<androidx.recyclerview.widget.RecyclerView
adapter="@{viewModel.adapter}"
dividerHorizontal="@{R.drawable.divider_l_50}"
dividerVertical="@{R.drawable.divider_l_50}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
android:id="@+id/settings_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="false"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:paddingEnd="@dimen/l_50"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
app:spanCount="2"
tools:layout_marginTop="24dp"
tools:listitem="@layout/item_settings_toggle"
tools:paddingTop="@dimen/l1" />
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.superuser.SuperuserViewModel" />
......@@ -15,39 +18,48 @@
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/superuser_content"
dividerColor="@{@android:color/transparent}"
dividerSize="@{@dimen/margin_generic}"
gone="@{!viewModel.loaded}"
android:id="@+id/superuser_list"
adapter="@{viewModel.adapter}"
dividerHorizontal="@{R.drawable.divider_l1}"
dividerVertical="@{R.drawable.divider_l1}"
goneUnless="@{viewModel.loaded || !viewModel.items.empty}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
nestedScrollingEnabled="@{false}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/margin_generic"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_policy" />
<ProgressBar
style="@style/Widget.Progress"
gone="@{!viewModel.loading}"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/superuser_empty"
gone="@{!viewModel.loaded || viewModel.items.size &gt; 0}"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l2}"
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
app:spanCount="2"
tools:layout_marginTop="24dp"
tools:listitem="@layout/item_policy_md2"
tools:paddingTop="@dimen/l1" />
<LinearLayout
goneUnless="@{viewModel.loading &amp;&amp; viewModel.items.empty}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/no_apps_found"
android:textSize="20sp"
android:textStyle="italic" />
android:orientation="vertical">
</FrameLayout>
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/safetynet_attest_loading"
android:textAppearance="@style/AppearanceFoundation.Title"
android:textStyle="bold" />
<ProgressBar
style="@style/WidgetFoundation.ProgressBar.Indeterminate"
android:layout_marginTop="@dimen/l1" />
</layout>
</LinearLayout>
</FrameLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.topjohnwu.magisk.ui.theme.Theme" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.theme.ThemeViewModel" />
</data>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l1}">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:orientation="vertical"
android:useDefaultMargins="true">
<include
item="@{viewModel.themeHeadline}"
android:id="@+id/theme_card_dark"
layout="@layout/item_tappable_headline"
listener="@{viewModel}" />
<FrameLayout
android:id="@+id/theme_default"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:theme="@style/ThemeFoundationMD2.Piplup"
app:layout_constraintEnd_toStartOf="@+id/theme_amoled"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/theme_card_dark">
<include
layout="@layout/item_theme"
theme="@{Theme.Piplup}"
viewModel="@{viewModel}" />
</FrameLayout>
<FrameLayout
android:id="@+id/theme_amoled"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:theme="@style/ThemeFoundationMD2.Amoled"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/theme_default"
app:layout_constraintTop_toBottomOf="@+id/theme_card_dark">
<include
layout="@layout/item_theme"
theme="@{Theme.PiplupAmoled}"
viewModel="@{viewModel}" />
</FrameLayout>
<FrameLayout
android:id="@+id/theme_rayquaza"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:theme="@style/ThemeFoundationMD2.Rayquaza"
app:layout_constraintEnd_toEndOf="@+id/theme_default"
app:layout_constraintStart_toStartOf="@+id/theme_default"
app:layout_constraintTop_toBottomOf="@+id/theme_default">
<include
layout="@layout/item_theme"
theme="@{Theme.Rayquaza}"
viewModel="@{viewModel}" />
</FrameLayout>
<FrameLayout
android:id="@+id/theme_zapdos"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:theme="@style/ThemeFoundationMD2.Zapdos"
app:layout_constraintEnd_toEndOf="@+id/theme_amoled"
app:layout_constraintStart_toStartOf="@+id/theme_amoled"
app:layout_constraintTop_toBottomOf="@+id/theme_amoled">
<include
layout="@layout/item_theme"
theme="@{Theme.Zapdos}"
viewModel="@{viewModel}" />
</FrameLayout>
<FrameLayout
android:id="@+id/theme_charmeleon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:theme="@style/ThemeFoundationMD2.Charmeleon"
app:layout_constraintEnd_toEndOf="@+id/theme_rayquaza"
app:layout_constraintStart_toStartOf="@+id/theme_rayquaza"
app:layout_constraintTop_toBottomOf="@+id/theme_rayquaza">
<include
layout="@layout/item_theme"
theme="@{Theme.Charmeleon}"
viewModel="@{viewModel}" />
</FrameLayout>
<FrameLayout
android:id="@+id/theme_mew"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:theme="@style/ThemeFoundationMD2.Mew"
app:layout_constraintEnd_toEndOf="@+id/theme_zapdos"
app:layout_constraintStart_toStartOf="@+id/theme_zapdos"
app:layout_constraintTop_toBottomOf="@+id/theme_zapdos">
<include
layout="@layout/item_theme"
theme="@{Theme.Mew}"
viewModel="@{viewModel}" />
</FrameLayout>
<FrameLayout
android:id="@+id/theme_salamence"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:theme="@style/ThemeFoundationMD2.Salamence"
app:layout_constraintEnd_toEndOf="@+id/theme_charmeleon"
app:layout_constraintStart_toStartOf="@+id/theme_charmeleon"
app:layout_constraintTop_toBottomOf="@+id/theme_charmeleon">
<include
layout="@layout/item_theme"
theme="@{Theme.Salamence}"
viewModel="@{viewModel}" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.hide.HideViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/l1"
android:paddingTop="@dimen/l1"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
tools:layout_gravity="bottom"
tools:paddingBottom="64dp">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/hide_filter_title_filter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hide_filters"
android:textAllCaps="true"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:textColor="?colorPrimary"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/hide_filter_chip_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
app:chipSpacing="2dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/hide_filter_title_filter">
<com.google.android.material.chip.Chip
style="@style/Widget.MaterialComponents.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={viewModel.showSystem}"
android:text="@string/show_system_app"
android:textAppearance="@style/AppearanceFoundation.Caption"
app:checkedIcon="@drawable/ic_check_md2"
app:chipBackgroundColor="?colorSurfaceVariant"
tools:checked="true" />
<com.google.android.material.chip.Chip
style="@style/Widget.MaterialComponents.Chip.Entry"
gone="@{viewModel.query.empty}"
onCloseClicked="@{() -> viewModel.resetQuery()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkable="false"
android:text="@{viewModel.query}"
android:textAppearance="@style/AppearanceFoundation.Caption"
app:chipBackgroundColor="?colorSurfaceVariant" />
</com.google.android.material.chip.ChipGroup>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/hide_filter_title_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:text="@string/hide_search"
android:textAllCaps="true"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:textColor="?colorPrimary"
android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@+id/hide_filter_chip_group" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/hide_filter_search"
style="@style/WidgetFoundation.Card"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
app:cardCornerRadius="18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/hide_filter_done"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/hide_filter_title_search">
<androidx.appcompat.widget.AppCompatImageView
style="@style/WidgetFoundation.Icon"
android:layout_width="48dp"
android:layout_height="36dp"
android:layout_gravity="center_vertical|start"
android:padding="6dp"
app:srcCompat="@drawable/ic_search_md2"
app:tint="?colorDisabled" />
<androidx.appcompat.widget.AppCompatEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:background="@null"
android:hint="@string/hide_filter_hint"
android:inputType="textUri"
android:minHeight="36dp"
android:gravity="start|center_vertical"
android:paddingStart="0dp"
android:paddingEnd="@dimen/l1"
android:singleLine="true"
android:text="@={viewModel.query}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textColor="@color/color_text_transient"
android:textColorHint="?colorOnSurfaceVariant" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/hide_filter_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:backgroundTint="?colorPrimary"
app:tint="?colorOnPrimary"
app:elevation="0dp"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="@+id/hide_filter_search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/hide_filter_search"
app:srcCompat="@drawable/ic_check_md2" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.core.Info" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:layout_gravity="center">
<com.google.android.material.checkbox.MaterialCheckBox
style="@style/WidgetFoundation.Checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:checked="@={Info.keepVerity}"
android:gravity="center"
android:text="@string/keep_dm_verity"
tools:checked="true" />
<com.google.android.material.checkbox.MaterialCheckBox
style="@style/WidgetFoundation.Checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:checked="@={Info.keepEnc}"
android:gravity="center"
android:text="@string/keep_force_encryption"
app:tint="?colorPrimary" />
<com.google.android.material.checkbox.MaterialCheckBox
style="@style/WidgetFoundation.Checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:checked="@={Info.recovery}"
android:gravity="center"
android:text="@string/recovery_mode"
app:tint="?colorPrimary" />
</LinearLayout>
</ScrollView>
</layout>
......@@ -5,10 +5,6 @@
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.MagiskLogRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.log.LogViewModel" />
......@@ -24,33 +20,35 @@
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
adapter="@{viewModel.itemsAdapter}"
itemBinding="@{viewModel.itemBinding}"
items="@{item.items}"
scrollPosition="@={viewModel.scrollPosition}"
scrollPositionSmooth="@{true}"
android:focusable="true"
adapter="@{viewModel.consoleAdapter}"
itemBinding="@{viewModel.itemConsoleBinding}"
items="@{viewModel.itemsConsole}"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:paddingBottom="@{viewModel.insets.bottom}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_console" />
tools:listitem="@layout/item_console"
tools:paddingTop="24dp" />
</HorizontalScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
hide="@{viewModel.scrollPosition == item.items.size - 1 || item.items.size == 0}"
android:focusable="true"
android:clickable="true"
android:layout_width="wrap_content"
<FrameLayout
gone="@{!viewModel.itemsConsole.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/margin_generic"
android:onClick="@{() -> viewModel.scrollDownPressed()}"
app:srcCompat="@drawable/ic_arrow_down"
app:tint="@color/colorTextInverse" />
android:layout_gravity="center">
</FrameLayout>
<include
item="@{viewModel.itemMagiskEmpty}"
layout="@layout/item_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
</FrameLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
tools:layout_gravity="bottom"
tools:paddingBottom="64dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/module_filter_list"
dividerHorizontal="@{R.drawable.divider_l1}"
dividerVertical="@{R.drawable.divider_l1}"
itemBinding="@{viewModel.itemSearchBinding}"
items="@{viewModel.itemsSearch}"
android:layout_width="match_parent"
android:layout_height="0dp"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/fast_scroll_thumb"
app:fastScrollHorizontalTrackDrawable="@drawable/fast_scroll_track"
app:fastScrollVerticalThumbDrawable="@drawable/fast_scroll_thumb"
app:fastScrollVerticalTrackDrawable="@drawable/fast_scroll_track"
android:layout_marginBottom="@dimen/l1"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@+id/module_filter_title_search"
app:layout_constraintTop_toTopOf="parent"
app:reverseLayout="false"
app:spanCount="2"
tools:listitem="@layout/item_repo_md2" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/l_50"
android:background="@drawable/bg_shadow"
app:layout_constraintBottom_toBottomOf="@+id/module_filter_list" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/module_filter_title_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l1"
android:text="@string/hide_search"
android:textAllCaps="true"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:textColor="?colorPrimary"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/module_filter_search" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/module_filter_search"
style="@style/WidgetFoundation.Card"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
app:cardCornerRadius="18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/module_filter_done"
app:layout_constraintStart_toStartOf="parent">
<androidx.appcompat.widget.AppCompatImageView
style="@style/WidgetFoundation.Icon"
android:layout_width="48dp"
android:layout_height="36dp"
android:layout_gravity="center_vertical|start"
android:padding="6dp"
app:srcCompat="@drawable/ic_search_md2"
app:tint="?colorDisabled" />
<androidx.appcompat.widget.AppCompatEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:background="@null"
android:hint="@string/hide_filter_hint"
android:inputType="textUri"
android:minHeight="36dp"
android:paddingStart="0dp"
android:paddingEnd="@dimen/l1"
android:singleLine="true"
android:text="@={viewModel.query}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textColor="@color/color_text_transient"
android:textColorHint="?colorOnSurfaceVariant" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/module_filter_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/l1"
app:backgroundTint="?colorPrimary"
app:elevation="0dp"
app:tint="?colorOnPrimary"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="@+id/module_filter_search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/module_filter_search"
app:srcCompat="@drawable/ic_check_md2" />
<ProgressBar
style="@style/WidgetFoundation.ProgressBar.Indeterminate.Circular"
goneUnless="@{viewModel.searchLoading}"
android:layout_width="56dp"
android:layout_height="56dp"
app:layout_constraintBottom_toBottomOf="@+id/module_filter_done"
app:layout_constraintEnd_toEndOf="@+id/module_filter_done"
app:layout_constraintStart_toStartOf="@+id/module_filter_done"
app:layout_constraintTop_toTopOf="@+id/module_filter_done" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.ui.home.MagiskState" />
<import type="com.topjohnwu.magisk.ui.home.MagiskItem" />
<import type="com.topjohnwu.magisk.R" />
<variable
name="item"
type="com.topjohnwu.magisk.ui.home.MagiskItem" />
<variable
name="state"
type="com.topjohnwu.magisk.ui.home.MagiskState" />
<variable
name="text"
type="String" />
<variable
name="currentVersion"
type="String" />
<variable
name="latestVersion"
type="String" />
<variable
name="additionalInfo"
type="String" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.home.HomeViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:background="?android:attr/selectableItemBackground"
android:onClick="@{() -> viewModel.cardPressed(item)}">
<com.google.android.material.button.MaterialButton
android:id="@+id/install"
style="@style/Widget.Button.Text"
gone="@{state == MagiskState.LOADING || !viewModel.isConnected}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:onClick="@{() -> viewModel.installPressed(item)}"
android:text="@{state != MagiskState.OBSOLETE ? @string/install : @string/update}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/install" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/status_icon"
invisible="@{state == MagiskState.LOADING}"
srcCompat="@{state == MagiskState.UP_TO_DATE ? R.drawable.ic_check_circle : (state == MagiskState.OBSOLETE ? R.drawable.ic_update : R.drawable.ic_help)}"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/status"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@{state == MagiskState.UP_TO_DATE ? @color/colorCorrect : (state == MagiskState.OBSOLETE ? @color/colorUpdate : @color/colorError)}" />
<ProgressBar
android:id="@+id/progress"
style="@style/Widget.Progress"
gone="@{state != MagiskState.LOADING}"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/status_icon"
app:layout_constraintEnd_toEndOf="@+id/status_icon"
app:layout_constraintStart_toStartOf="@+id/status_icon"
app:layout_constraintTop_toTopOf="@+id/status_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
style="@style/Widget.Text.Emphasize"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:text="@{text}"
android:maxLines="2"
app:layout_constraintBottom_toTopOf="@+id/current_version"
app:layout_constraintEnd_toStartOf="@+id/install"
app:layout_constraintStart_toEndOf="@+id/status_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/current_version"
gone="@{state == MagiskState.LOADING || currentVersion.length == 0}"
android:layout_width="0dp"
style="@style/Widget.Text.Caption.Inactive"
android:layout_height="wrap_content"
android:maxLines="2"
android:text="@{state != MagiskState.LOADING ? @string/current_installed(currentVersion) : @string/checking_for_updates}"
app:layout_constraintBottom_toTopOf="@+id/latest_version"
app:layout_constraintEnd_toEndOf="@+id/status"
app:layout_constraintStart_toStartOf="@+id/status"
app:layout_constraintTop_toBottomOf="@+id/status"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/latest_version"
gone="@{currentVersion == latestVersion || !viewModel.isConnected}"
android:layout_width="0dp"
style="@style/Widget.Text.Caption.Inactive"
android:layout_height="wrap_content"
android:maxLines="2"
android:text="@{state != MagiskState.LOADING ? @string/latest_version(latestVersion) : @string/checking_for_updates}"
app:layout_constraintBottom_toTopOf="@+id/additional"
app:layout_constraintEnd_toEndOf="@+id/status"
app:layout_constraintStart_toStartOf="@+id/status"
app:layout_constraintTop_toBottomOf="@+id/current_version"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/additional"
gone="@{additionalInfo.length == 0}"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Widget.Text.Caption.Inactive"
android:maxLines="2"
android:text="@{additionalInfo}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/status"
app:layout_constraintStart_toStartOf="@+id/status"
app:layout_constraintTop_toBottomOf="@+id/latest_version"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem" />
<!--no actions are required-->
<variable
name="viewModel"
type="Object" />
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:text="@{item.item}"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:textSize="10sp"
tools:text="@tools:sample/lorem/random" />
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.DeveloperItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.home.HomeViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:layout_gravity="center|start">
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/l1">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.name}"
android:textAppearance="@style/AppearanceFoundation.Caption.OnPrimary"
android:textStyle="bold"
tools:text="\@topjohnwu" />
<androidx.recyclerview.widget.RecyclerView
itemBinding="@{viewModel.itemBinding}"
items="@{item.items}"
nestedScrollingEnabled="@{false}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:fadingEdgeLength="@dimen/l1"
android:orientation="horizontal"
android:paddingTop="@dimen/l_50"
android:requiresFadingEdge="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/item_developer_link" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.HomeItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.home.HomeViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:onClick="@{() -> viewModel.onLinkPressed(item.link)}"
android:padding="@dimen/l_50"
tools:layout_gravity="center">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/developer_link"
style="@style/WidgetFoundation.Image.Small"
app:layout_constraintEnd_toStartOf="@+id/developer_more"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:srcCompat="@{item.icon}"
app:tint="?colorOnPrimary"
tools:srcCompat="@drawable/ic_paypal" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/developer_more"
style="@style/WidgetFoundation.Image.Small"
android:layout_height="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@+id/developer_link"
app:layout_constraintStart_toEndOf="@+id/developer_link"
app:layout_constraintTop_toTopOf="@+id/developer_link"
app:srcCompat="@drawable/ic_more"
app:tint="?colorOnPrimary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.HideRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.hide.HideViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:foreground="?attr/selectableItemBackground"
android:minHeight="?android:attr/listPreferredItemHeight"
android:onClick="@{() -> item.toggleExpansion()}"
app:cardElevation="@dimen/card_elevation">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/margin_generic_half">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:gravity="end"
android:src="@{item.item.icon}"
app:layout_constraintBottom_toTopOf="@+id/hide_app_processes"
app:layout_constraintEnd_toStartOf="@+id/hide_app_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_magisk" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/hide_app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_generic"
android:layout_marginEnd="@dimen/margin_generic"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.item.name}"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/hide_app_package"
app:layout_constraintEnd_toStartOf="@+id/hide_app_checkbox"
app:layout_constraintStart_toEndOf="@+id/hide_app_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Magisk" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/hide_app_package"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.packageName}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/hide_app_processes"
app:layout_constraintEnd_toStartOf="@id/hide_app_arrow"
app:layout_constraintStart_toStartOf="@+id/hide_app_name"
app:layout_constraintTop_toBottomOf="@+id/hide_app_name"
tools:text="com.topjohnwu.magisk" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_app_arrow"
gone="@{item.items.size == 1}"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:background="@android:color/transparent"
android:rotation="@{item.isExpanded() ? 180 : 0}"
app:layout_constraintBottom_toBottomOf="@+id/hide_app_package"
app:layout_constraintEnd_toEndOf="@+id/hide_app_name"
app:layout_constraintStart_toEndOf="@+id/hide_app_package"
app:layout_constraintTop_toTopOf="@+id/hide_app_package"
app:srcCompat="@drawable/ic_arrow"
app:tint="?attr/imageColorTint" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_app_checkbox"
android:focusable="true"
android:clickable="true"
style="@style/Widget.Icon"
isChecked="@{item.isHiddenState}"
android:onClick="@{() -> item.toggle()}"
app:layout_constraintBottom_toTopOf="@+id/hide_app_processes"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/hide_app_name"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/imageColorTint"
tools:src="@drawable/ic_checked" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/hide_app_processes"
gone="@{!item.isExpanded}"
itemBinding="@{viewModel.itemBinding}"
items="@{item.items}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/margin_generic_half"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
tools:listitem="@layout/item_hide_process" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.HideItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.hide.HideViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout_gravity="center"
tools:layout_marginBottom="@dimen/l1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:onClick="@{(v) -> item.toggle(v)}">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_icon"
style="@style/WidgetFoundation.Image"
android:layout_margin="@dimen/l1"
android:alpha="@{item.isExpanded ? 0.1f : 1f}"
android:src="@{item.item.info.icon}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
tools:src="@drawable/ic_magisk" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_action"
style="@style/WidgetFoundation.Image"
isSelected="@{item.itemsCheckedPercent == 100}"
goneUnless="@{item.isExpanded}"
android:layout_margin="@dimen/l1"
android:background="?selectableItemBackgroundBorderless"
android:onClick="@{() -> item.toggle(viewModel)}"
android:padding="@dimen/l_25"
app:layout_constraintBottom_toBottomOf="@+id/hide_icon"
app:layout_constraintEnd_toEndOf="@+id/hide_icon"
app:layout_constraintStart_toStartOf="@+id/hide_icon"
app:layout_constraintTop_toTopOf="@+id/hide_icon"
app:srcCompat="@drawable/ic_hide_select_md2" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/hide_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:ellipsize="middle"
android:singleLine="true"
android:text="@{item.item.info.name}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/hide_package"
app:layout_constraintEnd_toStartOf="@+id/hide_expand_icon"
app:layout_constraintStart_toEndOf="@+id/hide_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/app_name" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/hide_package"
gone="@{item.item.info.info.packageName.empty}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.item.info.info.packageName}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/hide_name"
app:layout_constraintStart_toStartOf="@+id/hide_name"
app:layout_constraintTop_toBottomOf="@+id/hide_name"
tools:text="com.topjohnwu.magisk" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_expand_icon"
style="@style/WidgetFoundation.Icon"
invisible="@{item.item.processes.empty}"
srcCompat="@{item.isExpanded ? R.drawable.ic_close_md2 : R.drawable.ic_back_md2}"
android:background="@null"
android:rotation="180"
android:rotationX="@{item.isExpanded ? 180 : 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/ic_back_md2" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
goneUnless="@{item.isExpanded}"
itemBinding="@{viewModel.itemInternalBinding}"
items="@{item.items}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorSurfaceVariant"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="2"
tools:listitem="@layout/item_hide_process_md2" />
</LinearLayout>
<ProgressBar
style="@style/WidgetFoundation.ProgressBar"
android:layout_width="match_parent"
android:layout_gravity="top"
android:progress="@{item.itemsCheckedPercent}" />
</com.google.android.material.card.MaterialCardView>
</layout>
......@@ -7,7 +7,7 @@
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem" />
type="com.topjohnwu.magisk.model.entity.recycler.HideProcessItem" />
<variable
name="viewModel"
......@@ -17,40 +17,41 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="false"
android:focusable="false"
android:onClick="@{() -> item.toggle()}">
android:onClick="@{() -> item.toggle(viewModel)}"
android:layout_height="wrap_content"
android:layout_gravity="center">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_generic"
android:ellipsize="marquee"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
android:singleLine="true"
android:text="@{item.process}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
android:text="@{item.item.name}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/hide_process_icon"
app:layout_constraintEnd_toStartOf="@+id/hide_process_checkbox"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="com.topjohnwu.magisk.process" />
tools:text="com.topjohnwu.magisk" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_process_icon"
style="@style/Widget.Icon"
isChecked="@{item.isHidden}"
android:background="@null"
android:clickable="false"
android:focusable="false"
android:id="@+id/hide_process_checkbox"
style="@style/WidgetFoundation.Image.Small"
android:layout_width="18dp"
android:layout_height="18dp"
isSelected="@{item.isHidden}"
android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/imageColorTint"
tools:src="@drawable/ic_checked" />
app:srcCompat="@drawable/ic_radio_check_button"
app:tint="?colorPrimary" />
</androidx.constraintlayout.widget.ConstraintLayout>
......
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.view.MagiskDialog.DialogItem" />
<variable
name="listener"
type="com.topjohnwu.magisk.view.MagiskDialog.ActualOnDialogClickListener" />
</data>
<androidx.appcompat.widget.AppCompatTextView
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:onClick="@{() -> listener.onClick(item.position)}"
android:paddingStart="@dimen/l1"
android:paddingTop="@dimen/l_75"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@dimen/l_75"
android:text="@{item.item}"
android:textAppearance="@style/AppearanceFoundation.Body"
tools:text="@tools:sample/lorem" />
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.LogItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.log.LogViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?listPreferredItemHeightSmall"
tools:layout_gravity="center">
<include
android:id="@+id/log_track_container"
bullet="@{item.item.action ? R.drawable.ic_check_md2 : R.drawable.ic_close_md2}"
isBottom="@{item.isBottom}"
isSelected="@{!item.item.action}"
isTop="@{item.isTop}"
layout="@layout/item_log_track_md2"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:paddingTop="@dimen/l_50"
android:paddingBottom="@dimen/l_50"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/log_track_container"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/log_app_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.item.appName}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/log_date"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/app_name" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/log_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.date}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
android:textSize="10sp"
app:layout_constraintBottom_toTopOf="@+id/log_app_details"
app:layout_constraintTop_toBottomOf="@+id/log_app_name"
tools:text="06:00 PM, 10 Oct 2019" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/log_app_details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{String.format(`%s %s`, @string/pid(item.item.fromPid), @string/target_uid(item.item.toUid))}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/log_command"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/log_date"
tools:text="PID: 7196 Target UID: 0" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/log_command"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:text="@{item.item.command}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
android:textSize="10sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/log_app_details"
app:layout_constraintTop_toTopOf="@+id/log_app_details"
tools:text="/system/bin/sh" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="isTop"
type="Boolean" />
<variable
name="isBottom"
type="Boolean" />
<variable
name="bullet"
type="Integer" />
<variable
name="isSelected"
type="Boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:layout_gravity="center"
tools:minHeight="?listPreferredItemHeightSmall">
<View
android:id="@+id/track_top"
invisible="@{isTop}"
android:layout_width="2dp"
android:layout_height="0dp"
android:layout_marginBottom="2dp"
android:alpha=".3"
android:background="@drawable/bg_line_bottom_rounded"
app:layout_constraintBottom_toTopOf="@+id/track_bullet"
app:layout_constraintEnd_toEndOf="@+id/track_bullet"
app:layout_constraintStart_toStartOf="@+id/track_bullet"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/track_bullet"
style="@style/WidgetFoundation.Image.Small"
isSelected="@{isSelected}"
srcCompat="@{bullet}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/color_primary_error_transient"
tools:srcCompat="@drawable/ic_check_md2" />
<View
android:id="@+id/track_bottom"
invisible="@{isBottom}"
android:layout_width="2dp"
android:layout_height="0dp"
android:layout_marginTop="2dp"
android:alpha=".3"
android:background="@drawable/bg_line_top_rounded"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/track_bullet"
app:layout_constraintStart_toStartOf="@+id/track_bullet"
app:layout_constraintTop_toBottomOf="@+id/track_bullet" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:minHeight="?android:attr/listPreferredItemHeight"
app:cardElevation="2dp"
tools:layout_gravity="center">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_generic">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.item.name}"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/version_name"
app:layout_constraintEnd_toStartOf="@+id/checkbox"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Magisk Module" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/version_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.item.version.length == 0 ? @string/no_info_provided : item.item.version}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
android:textStyle="bold|italic"
app:layout_constraintBottom_toTopOf="@+id/author"
app:layout_constraintEnd_toEndOf="@+id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="v1" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.item.author.length == 0 ? @string/no_info_provided : @string/author(item.item.author)}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
android:textStyle="bold|italic"
app:layout_constraintBottom_toTopOf="@+id/description"
app:layout_constraintEnd_toEndOf="@+id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/version_name"
tools:text="topjohnwu" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@{item.item.description.length == 0 ? @string/no_info_provided : item.item.description}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/notice"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/author"
tools:lines="4"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/notice"
gone="@{item.lastActionNotice.length == 0}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@{item.lastActionNotice}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/red500"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/description" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/checkbox"
android:focusable="true"
android:clickable="true"
style="@style/Widget.Icon"
isChecked="@{item.isChecked}"
android:layout_marginEnd="@dimen/margin_generic"
android:onClick="@{() -> item.toggle()}"
app:layout_constraintBottom_toTopOf="@+id/description"
app:layout_constraintEnd_toStartOf="@+id/delete"
app:layout_constraintStart_toEndOf="@+id/title"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/imageColorTint"
tools:src="@drawable/ic_checked" />
<View
style="@style/Widget.Divider.Vertical"
android:backgroundTint="?attr/imageColorTint"
app:layout_constraintBottom_toBottomOf="@+id/checkbox"
app:layout_constraintEnd_toStartOf="@+id/delete"
app:layout_constraintStart_toEndOf="@+id/checkbox"
app:layout_constraintTop_toTopOf="@+id/checkbox" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/delete"
android:focusable="true"
android:clickable="true"
style="@style/Widget.Icon"
srcCompat="@{item.isDeletable ? R.drawable.ic_undelete : R.drawable.ic_delete}"
android:onClick="@{() -> item.toggleDelete()}"
app:layout_constraintBottom_toBottomOf="@+id/checkbox"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/checkbox"
app:layout_constraintTop_toTopOf="@+id/checkbox"
app:tint="?attr/imageColorTint"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_delete" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.InstallModule" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<com.google.android.material.button.MaterialButton
style="@style/WidgetFoundation.Button.Outlined"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.installPressed()}"
android:text="@string/module_action_install_external"
android:textAllCaps="false"
android:textSize="12sp"
app:cornerRadius="@dimen/r1"
app:icon="@drawable/ic_module_storage_md2" />
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<import type="com.topjohnwu.magisk.core.Config" />
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.ModuleItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card.Variant"
isEnabled="@{!Config.coreOnly &amp;&amp; !item.removed}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="@{item.enabled &amp;&amp; !Config.coreOnly ? 1f : .5f}"
android:onClick="@{() -> item.toggle()}"
tools:layout_gravity="center"
tools:layout_marginBottom="@dimen/l1"
tools:layout_marginEnd="@dimen/l1">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/module_state_icon"
style="@style/WidgetFoundation.Image.Small"
gone="@{!item.removed &amp;&amp; !item.updated}"
srcCompat="@{item.removed ? R.drawable.ic_delete_md2 : (item.updated ? R.drawable.ic_update_md2 : 0)}"
android:layout_marginStart="@dimen/l1"
android:background="@null"
app:layout_constraintBottom_toBottomOf="@+id/module_version_author"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/module_title"
tools:srcCompat="@drawable/ic_update_md2" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/module_title"
strikeThrough="@{!item.enabled}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:text="@{item.item.name}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/module_state_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/module_version_author"
strikeThrough="@{!item.enabled}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:text="@{@string/module_version_author(item.item.version, item.item.author)}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/module_state_icon"
app:layout_constraintTop_toBottomOf="@+id/module_title"
tools:text="v1 by topjohnwu" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/module_description"
gone="@{item.item.description.empty}"
strikeThrough="@{!item.enabled}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:text="@{item.item.description}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintTop_toBottomOf="@+id/module_version_author"
tools:lines="4"
tools:text="@tools:sample/lorem/random" />
<View
android:id="@+id/module_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="@dimen/l1"
android:background="?colorSurfaceVariant"
app:layout_constraintTop_toBottomOf="@+id/module_description" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/module_info"
style="@style/WidgetFoundation.Icon"
gone="@{item.repo == null}"
android:layout_width="wrap_content"
android:alpha=".5"
android:onClick="@{() -> viewModel.infoPressed(item)}"
android:paddingEnd="@dimen/l_50"
app:layout_constraintBottom_toBottomOf="@+id/module_remove"
app:layout_constraintEnd_toStartOf="@+id/module_remove"
app:layout_constraintTop_toTopOf="@+id/module_remove"
app:srcCompat="@drawable/ic_info" />
<com.google.android.material.button.MaterialButton
android:id="@+id/module_remove"
style="@style/WidgetFoundation.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{(v) -> item.delete(viewModel)}"
android:text="@{item.removed ? @string/module_state_restore : @string/module_state_remove}"
android:textAllCaps="false"
app:icon="@{item.removed ? R.drawable.ic_restart : R.drawable.ic_delete_md2}"
app:iconGravity="textEnd"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/module_divider"
tools:icon="@drawable/ic_delete_md2"
tools:text="Remove" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.LogRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.log.LogViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
dividerColor="@{@android:color/transparent}"
dividerSize="@{@dimen/margin_generic}"
itemBinding="@{viewModel.itemBinding}"
items="@{item.items}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/margin_generic"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_superuser_log" />
</FrameLayout>
</layout>
\ No newline at end of file
......@@ -7,7 +7,7 @@
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem" />
type="com.topjohnwu.magisk.model.entity.recycler.PolicyItem" />
<variable
name="viewModel"
......@@ -16,191 +16,163 @@
</data>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Card"
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:foreground="?attr/selectableItemBackground"
android:minHeight="?android:attr/listPreferredItemHeight"
android:onClick="@{() -> item.toggle()}"
app:cardCornerRadius="@dimen/radius_generic"
app:cardElevation="2dp"
app:cardPreventCornerOverlap="true">
android:alpha="@{item.isEnabled() ? 1f : .5f}"
android:onClick="@{() -> item.toggle(viewModel)}"
android:onLongClick="@{() -> item.toggle()}"
tools:layout_marginBottom="@dimen/l1"
tools:layout_marginEnd="@dimen/l1"
tools:layout_width="200dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/margin_generic_half">
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/app_icon"
android:layout_width="50dp"
android:layout_height="50dp"
android:gravity="end"
android:id="@+id/policy_app_icon"
style="@style/WidgetFoundation.Image"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginBottom="@dimen/l1"
android:src="@{item.icon}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/app_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
tools:srcCompat="@drawable/ic_logo" />
<androidx.appcompat.widget.AppCompatImageView
style="@style/WidgetFoundation.Image.Big"
gone="@{item.isEnabled()}"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/policy_app_icon"
app:layout_constraintEnd_toEndOf="@+id/policy_app_icon"
app:layout_constraintStart_toStartOf="@+id/policy_app_icon"
app:layout_constraintTop_toTopOf="@+id/policy_app_icon"
app:srcCompat="@drawable/ic_off"
app:tint="?colorSurface"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/app_name"
android:id="@+id/policy_app_name"
strikeThrough="@{!item.enabled}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_generic"
android:layout_marginRight="@dimen/margin_generic"
android:ellipsize="end"
android:maxLines="1"
android:ellipsize="middle"
android:gravity="start"
android:maxLines="2"
android:text="@{item.item.appName}"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/package_name"
app:layout_constraintEnd_toStartOf="@+id/master_switch"
app:layout_constraintStart_toEndOf="@+id/app_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/policy_app_icon"
app:layout_constraintTop_toTopOf="@+id/policy_app_icon"
tools:text="@string/app_name" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/package_name"
android:id="@+id/policy_package_name"
strikeThrough="@{!item.enabled}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_marginBottom="@dimen/l1"
android:ellipsize="middle"
android:gravity="start"
android:maxLines="2"
android:text="@{item.item.packageName}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/app_name"
app:layout_constraintStart_toStartOf="@id/app_name"
app:layout_constraintTop_toBottomOf="@id/app_name"
app:layout_constraintEnd_toEndOf="@id/policy_app_name"
app:layout_constraintStart_toStartOf="@id/policy_app_name"
app:layout_constraintTop_toBottomOf="@id/policy_app_name"
app:layout_constraintVertical_bias="0"
tools:text="com.topjohnwu.magisk" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/master_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:checked="@={item.isEnabled}"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/app_name"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/expand_layout"
android:id="@+id/policy_expand_container"
gone="@{!item.isExpanded}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/margin_generic_half"
android:paddingTop="@dimen/margin_generic_half"
android:layout_height="match_parent"
android:background="?colorSurfaceVariant"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/package_name">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/bell"
android:layout_width="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/policy_package_name"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton
android:id="@+id/policy_notify"
style="@style/WidgetFoundation.Button.Text"
isSelected="@{item.shouldNotify}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:tint="@color/icon_grey"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/notification_switch"
android:minHeight="24dp"
android:onClick="@{() -> item.toggleNotify(viewModel)}"
android:text="@string/superuser_toggle_notification"
android:textAllCaps="false"
android:textAppearance="@style/AppearanceFoundation.Tiny"
android:textColor="@color/color_state_primary_transient"
app:icon="@drawable/ic_notifications_md2"
app:iconTint="@color/color_state_primary_transient"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_notifications" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/notification_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:checked="@={item.shouldNotify}"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toEndOf="@+id/bell"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.4" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/bug"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:tint="@color/icon_grey"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/logging_switch"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_bug_report" />
app:tint="@color/color_state_primary_transient" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/logging_switch"
android:layout_width="wrap_content"
<com.google.android.material.button.MaterialButton
android:id="@+id/policy_log"
style="@style/WidgetFoundation.Button.Text"
isSelected="@{item.shouldLog}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:checked="@={item.shouldLog}"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toEndOf="@+id/bug"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.8" />
android:minHeight="24dp"
android:onClick="@{() -> item.toggleLog(viewModel)}"
android:text="@string/superuser_toggle_log"
android:textAllCaps="false"
android:textAppearance="@style/AppearanceFoundation.Tiny"
android:textColor="@color/color_state_primary_transient"
app:icon="@drawable/ic_bug_md2"
app:iconTint="@color/color_state_primary_transient"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/policy_notify"
app:tint="@color/color_state_primary_transient" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/more_info"
android:layout_width="wrap_content"
<com.google.android.material.button.MaterialButton
android:id="@+id/policy_delete"
style="@style/WidgetFoundation.Button.Text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:tint="@color/icon_grey"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/delete"
app:layout_constraintStart_toStartOf="@id/guideline2"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_more" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/delete"
style="@style/Widget.Icon"
android:layout_gravity="center"
android:layout_weight="1"
android:minHeight="24dp"
android:onClick="@{() -> viewModel.deletePressed(item)}"
android:tint="@color/icon_grey"
android:text="@string/superuser_toggle_revoke"
android:textAllCaps="false"
android:textColor="?colorError"
android:textSize="12sp"
app:icon="@drawable/ic_delete_md2"
app:iconTint="?colorError"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/more_info"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_delete" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/policy_log"
app:rippleColor="?colorError" />
</androidx.constraintlayout.widget.ConstraintLayout>
......@@ -208,4 +180,4 @@
</com.google.android.material.card.MaterialCardView>
</layout>
\ No newline at end of file
</layout>
......@@ -5,9 +5,11 @@
<data>
<import type="com.topjohnwu.magisk.R" />
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.RepoRvItem" />
type="com.topjohnwu.magisk.model.entity.recycler.RepoItem" />
<variable
name="viewModel"
......@@ -16,114 +18,132 @@
</data>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Card"
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:minHeight="?android:attr/listPreferredItemHeight"
android:onClick="@{() -> viewModel.repoPressed(item)}"
app:cardElevation="2dp"
tools:layout_gravity="center">
tools:layout_gravity="center"
tools:layout_marginBottom="@dimen/l1"
tools:layout_marginEnd="@dimen/l1">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_generic">
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:id="@+id/module_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:text="@{item.item.name}"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/version_name"
app:layout_constraintEnd_toStartOf="@+id/download"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Magisk Module" />
tools:text="@tools:sample/lorem" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/version_name"
android:id="@+id/module_version_author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.item.version.length == 0 ? @string/no_info_provided : item.item.version}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
android:textStyle="bold|italic"
app:layout_constraintBottom_toTopOf="@+id/author"
app:layout_constraintEnd_toEndOf="@+id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="v1" />
android:text="@{@string/module_version_author(item.item.version ?? `?`, item.item.author ?? `?`)}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintEnd_toEndOf="@+id/module_title"
app:layout_constraintStart_toStartOf="@+id/module_title"
app:layout_constraintTop_toBottomOf="@+id/module_title"
tools:text="v1 by topjohnwu" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/author"
android:layout_width="0dp"
android:id="@+id/module_description"
gone="@{item.item.description.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.item.author.length == 0 ? @string/no_info_provided : @string/author(item.item.author)}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
android:textStyle="bold|italic"
app:layout_constraintBottom_toTopOf="@+id/description"
app:layout_constraintEnd_toEndOf="@+id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/version_name"
tools:text="topjohnwu" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@{item.item.description.length == 0 ? @string/no_info_provided : item.item.description}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/update_time"
app:layout_constraintEnd_toEndOf="@id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/author"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:text="@{item.item.description}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintTop_toBottomOf="@+id/module_version_author"
tools:lines="4"
tools:text="@tools:sample/lorem/random" />
<View
android:id="@+id/module_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="@dimen/l1"
android:background="?colorSurfaceVariant"
app:layout_constraintTop_toBottomOf="@+id/module_description" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/update_time"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@{@string/updated_on(item.item.lastUpdateString)}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textStyle="bold|italic"
android:layout_marginStart="@dimen/l1"
android:text="@{item.item.lastUpdateString}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
android:textSize="11sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/description"
app:layout_constraintEnd_toStartOf="@+id/module_info"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/module_divider"
tools:ignore="SmallSp"
tools:text="@tools:sample/date/ddmmyy" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/download"
android:focusable="true"
android:clickable="true"
style="@style/Widget.Icon"
isEnabled="@{viewModel.isConnected}"
android:alpha="@{viewModel.isConnected ? 1f : .2f}"
android:id="@+id/module_info"
style="@style/WidgetFoundation.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:onClick="@{() -> viewModel.downloadPressed(item)}"
android:tint="?attr/imageColorTint"
android:alpha=".5"
android:onClick="@{() -> viewModel.infoPressed(item)}"
android:paddingEnd="@dimen/l_50"
app:layout_constraintBottom_toBottomOf="@+id/module_download"
app:layout_constraintEnd_toStartOf="@+id/module_download"
app:layout_constraintTop_toTopOf="@+id/module_download"
app:srcCompat="@drawable/ic_info" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/module_download"
android:layout_width="wrap_content"
style="@style/WidgetFoundation.Icon.Primary"
android:paddingStart="@dimen/l_50"
isEnabled="@{!(item.progress == -100 || (item.progress > 0 &amp;&amp; item.progress &lt; 100))}"
android:contentDescription="@string/download"
srcCompat="@{item.isUpdate ? R.drawable.ic_update_md2 : R.drawable.ic_download_md2}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/title"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_file_download_black" />
app:layout_constraintTop_toBottomOf="@+id/module_divider"
android:onClick="@{() -> viewModel.downloadPressed(item)}"
tools:srcCompat="@drawable/ic_download_md2" />
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom">
<ProgressBar
style="@style/WidgetFoundation.ProgressBar"
goneUnless="@{item.progress > 0 &amp;&amp; item.progress &lt; 100}"
progressAnimated="@{item.progress}"
android:layout_width="match_parent"
android:layout_gravity="bottom"
tools:progress="40" />
<androidx.core.widget.ContentLoadingProgressBar
style="@style/WidgetFoundation.ProgressBar.Indeterminate"
goneUnless="@{item.progress == -100}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="-4dp"
android:layout_marginBottom="-5dp"
tools:progress="40" />
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
......
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.SafeModeNotice" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?colorError">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="@dimen/l1"
android:text="@string/module_safe_mode_message"
android:textAppearance="@style/AppearanceFoundation.Caption.OnPrimary"
android:textStyle="bold" />
</com.google.android.material.card.MaterialCardView>
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.SectionTitle" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:animateLayoutChanges="true"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/module_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.title}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textColor="@color/color_primary_transient"
android:textStyle="bold"
tools:text="@tools:sample/lorem/random"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/module_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/module_button"
style="@style/WidgetFoundation.Button.Text.Secondary"
gone="@{!item.hasButton}"
android:onClick="@{() -> viewModel.sectionPressed(item)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:text="@{item.button}"
tools:text="@tools:sample/lorem"
android:textAllCaps="false"
app:icon="@{item.icon}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/module_title"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.SettingsItem.Blank" />
<variable
name="callback"
type="com.topjohnwu.magisk.model.entity.recycler.SettingsItem.Callback" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
isEnabled="@{item.enabled}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="@{item.enabled ? 1f : .5f}"
android:onClick="@{(view) -> callback.onItemChanged(view, item)}"
tools:layout_gravity="center">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/blank_icon"
style="@style/WidgetFoundation.Icon"
gone="@{item.icon == 0}"
android:background="@null"
android:rotation="180"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@{item.icon}"
tools:srcCompat="@drawable/ic_fingerprint" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/blank_indicator"
style="@style/WidgetFoundation.Icon"
android:background="@null"
android:rotation="180"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_back_md2" />
<LinearLayout
android:id="@+id/blank_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@{item.icon == 0 ? (int) @dimen/l1 : 0}"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingTop="@dimen/l1"
android:paddingBottom="@dimen/l1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/blank_indicator"
app:layout_constraintStart_toEndOf="@+id/blank_icon"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/blank_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="start"
android:text="@{item.title}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
tools:lines="2"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/blank_description"
gone="@{item.description.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.description}"
android:textAppearance="@style/AppearanceFoundation.Tiny.Variant"
tools:lines="2"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.SettingsItem.Input" />
<variable
name="callback"
type="com.topjohnwu.magisk.model.entity.recycler.SettingsItem.Callback" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
isEnabled="@{item.enabled}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="@{item.enabled ? 1f : .5f}"
android:onClick="@{(view) -> item.onPressed(view, callback)}"
tools:layout_gravity="center">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/input_icon"
style="@style/WidgetFoundation.Icon"
gone="@{item.icon == 0}"
android:background="@null"
android:rotation="180"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/input_divider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@{item.icon}"
tools:srcCompat="@drawable/ic_fingerprint" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/input_indicator"
style="@style/WidgetFoundation.Icon"
android:background="@null"
android:rotation="180"
app:layout_constraintBottom_toTopOf="@+id/input_divider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_back_md2" />
<LinearLayout
android:id="@+id/input_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@{item.icon == 0 ? (int) @dimen/l1 : 0}"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingTop="@dimen/l1"
android:paddingBottom="@dimen/l1"
app:layout_constraintBottom_toTopOf="@+id/input_divider"
app:layout_constraintEnd_toStartOf="@+id/input_indicator"
app:layout_constraintStart_toEndOf="@+id/input_icon"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/input_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="start"
android:text="@{item.title}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
tools:lines="1"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/input_description"
gone="@{item.description.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.description}"
android:textAppearance="@style/AppearanceFoundation.Tiny.Variant"
tools:lines="2"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
<View
android:id="@+id/input_divider"
gone="@{item.value.empty || !item.showStrip}"
android:layout_width="match_parent"
android:layout_height="0dp"
android:alpha=".5"
android:background="?colorSurface"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/input_text" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/input_selection"
gone="@{item.value.empty || !item.showStrip}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/l1"
android:paddingTop="@dimen/l_75"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@dimen/l_75"
android:text="@{item.value}"
android:textAppearance="@style/AppearanceFoundation.Tiny.Variant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/input_text"
app:layout_constraintTop_toTopOf="@+id/input_divider"
tools:text="@tools:sample/lorem" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.SettingsItem" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/l1"
android:paddingBottom="@dimen/l_50">
<androidx.appcompat.widget.AppCompatTextView
gone="@{item.title.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.title}"
android:textAppearance="@style/AppearanceFoundation.Display.Secondary"
android:textStyle="bold"
tools:text="@tools:sample/lorem" />
<androidx.appcompat.widget.AppCompatTextView
gone="@{item.description.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.description}"
android:textAppearance="@style/AppearanceFoundation.Tiny.Bold"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.SettingsItem.Selector" />
<variable
name="callback"
type="com.topjohnwu.magisk.model.entity.recycler.SettingsItem.Callback" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
isEnabled="@{item.enabled}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="@{item.enabled ? 1f : .5f}"
android:onClick="@{(view) -> item.onPressed(view, callback)}"
tools:layout_gravity="center">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/selector_icon"
style="@style/WidgetFoundation.Icon"
gone="@{item.icon == 0}"
android:background="@null"
android:rotation="180"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/selector_divider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@{item.icon}"
tools:srcCompat="@drawable/ic_fingerprint" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/selector_indicator"
style="@style/WidgetFoundation.Icon"
android:background="@null"
android:rotation="180"
app:layout_constraintBottom_toTopOf="@+id/selector_divider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_back_md2" />
<LinearLayout
android:id="@+id/selector_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@{item.icon == 0 ? (int) @dimen/l1 : 0}"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingTop="@dimen/l1"
android:paddingBottom="@dimen/l1"
app:layout_constraintBottom_toTopOf="@+id/selector_divider"
app:layout_constraintEnd_toStartOf="@+id/selector_indicator"
app:layout_constraintStart_toEndOf="@+id/selector_icon"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/selector_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="start"
android:text="@{item.title}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
tools:lines="1"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/selector_description"
gone="@{item.description.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.description}"
android:textAppearance="@style/AppearanceFoundation.Tiny.Variant"
tools:lines="2"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
<View
android:id="@+id/selector_divider"
gone="@{item.selectedEntry == null}"
android:layout_width="match_parent"
android:layout_height="0dp"
android:alpha=".5"
android:background="?colorSurface"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/selector_text" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/selector_selection"
gone="@{item.selectedEntry == null}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/l1"
android:paddingTop="@dimen/l_75"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@dimen/l_75"
android:text="@{item.selectedEntry}"
android:textAppearance="@style/AppearanceFoundation.Tiny.Variant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/selector_text"
app:layout_constraintTop_toTopOf="@+id/selector_divider"
tools:text="@tools:sample/lorem" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.SettingsItem.Toggle" />
<variable
name="callback"
type="com.topjohnwu.magisk.model.entity.recycler.SettingsItem.Callback" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
isEnabled="@{item.enabled}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="@{item.enabled ? 1f : .5f}"
android:onClick="@{(view) -> item.onPressed(view, callback)}"
tools:layout_gravity="center">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/toggle_icon"
style="@style/WidgetFoundation.Icon"
gone="@{item.icon == 0}"
android:background="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@{item.icon}"
tools:srcCompat="@drawable/ic_fingerprint" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/toggle_indicator"
style="@style/WidgetFoundation.Switch"
isEnabled="@{item.enabled}"
onTouch="@{(view, event) -> item.onTouched(view, callback, event)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/l_25"
android:checked="@{item.value}"
android:text=""
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@{item.icon == 0 ? (int) @dimen/l1 : 0}"
android:layout_marginEnd="@dimen/l_50"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingTop="@dimen/l1"
android:paddingBottom="@dimen/l1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toggle_indicator"
app:layout_constraintStart_toEndOf="@+id/toggle_icon"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/toggle_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="start"
android:text="@{item.title}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
tools:lines="1"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/toggle_description"
gone="@{item.description.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.description}"
android:textAppearance="@style/AppearanceFoundation.Tiny.Variant"
tools:lines="2"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.LogItemRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.log.LogViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/log_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:onClick="@{() -> item.toggle()}"
android:orientation="horizontal"
android:paddingLeft="@dimen/margin_generic"
android:paddingTop="@dimen/margin_generic_half"
android:paddingRight="@dimen/margin_generic"
android:paddingBottom="@dimen/margin_generic_half"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatTextView
style="@style/Widget.Text.Emphasize"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center_vertical"
android:text="@{item.date}"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/log_header_indicator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/date/ddmmyy" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/log_header_indicator"
style="@style/Widget.Icon"
android:background="@android:color/transparent"
android:rotation="@{item.isExpanded() ? 180 : 0}"
android:tint="?attr/imageColorTint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow"
tools:rotation="180" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
gone="@{!item.isExpanded}"
itemBinding="@{viewModel.itemBinding}"
items="@{item.items}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@+id/log_header"
tools:itemCount="3"
tools:listitem="@layout/item_superuser_log_entry" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.LogItemEntryRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.log.LogViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:background="?attr/selectableItemBackground"
android:onClick="@{() -> item.toggle()}"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingLeft="@dimen/margin_generic"
android:paddingTop="@dimen/margin_generic_half"
android:paddingRight="@dimen/margin_generic"
android:paddingBottom="@dimen/margin_generic_half">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.item.appName}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary"
android:textIsSelectable="false"
tools:text="@tools:sample/lorem" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_horizontal"
android:text="@{item.item.action ? @string/grant : @string/deny}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
tools:text="@string/grant" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/time"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_horizontal"
android:text="@{item.item.timeString}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
tools:text="@tools:sample/date/hhmm" />
</LinearLayout>
<LinearLayout
gone="@{!item.isExpanded}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:orientation="horizontal"
android:paddingStart="@dimen/margin_generic"
android:paddingTop="@dimen/margin_generic_half"
android:paddingEnd="@dimen/margin_generic"
android:paddingBottom="@dimen/margin_generic_half">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@{@string/pid(item.item.fromPid)}"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp"
tools:text="@string/pid" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@{@string/target_uid(item.item.toUid)}"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp"
tools:text="@string/target_uid" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="@{@string/command(item.item.command)}"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp"
tools:text="@string/command" />
</LinearLayout>
</LinearLayout>
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.TappableHeadlineItem" />
<variable
name="listener"
type="com.topjohnwu.magisk.model.entity.recycler.TappableHeadlineItem.Listener" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> listener.onItemPressed(item)}"
tools:layout_gravity="center">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/tappable_icon"
style="@style/WidgetFoundation.Icon"
android:background="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@{item.icon}"
tools:srcCompat="@drawable/ic_hide_md2" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:requiresFadingEdge="horizontal"
android:singleLine="true"
android:text="@{item.title}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/headline_icon_pointer"
app:layout_constraintStart_toEndOf="@+id/tappable_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/magisk_hide_md2" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/headline_icon_pointer"
style="@style/WidgetFoundation.Icon"
android:background="@null"
android:rotation="180"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_back_md2" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.TextItem" />
</data>
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="@dimen/l1"
android:text="@{item.text}"
android:textAppearance="@style/AppearanceFoundation.Tiny.Variant"
tools:text="@tools:sample/lorem/random" />
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.theme.ThemeViewModel" />
<variable
name="theme"
type="com.topjohnwu.magisk.ui.theme.Theme" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.saveTheme(theme)}"
app:cardBackgroundColor="@android:color/transparent"
app:strokeColor="?colorPrimaryVariant"
tools:layout_gravity="center">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorSurface">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/theme_appbar"
style="@style/WidgetFoundation.Appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
style="@style/WidgetFoundation.Toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="100dp"
android:layout_height="17sp"
android:layout_gravity="center_vertical|start"
android:background="?colorOnSurface" />
<View
android:layout_width="70dp"
android:layout_height="12sp"
android:layout_gravity="center_vertical|start"
android:layout_marginTop="@dimen/l_25"
android:background="?colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/theme_card_bottom"
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/theme_appbar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/l1">
<View
android:layout_width="match_parent"
android:layout_height="17sp"
android:background="?colorOnSurface" />
<View
android:layout_width="40dp"
android:layout_height="11sp"
android:layout_marginTop="@dimen/l_25"
android:background="?colorOnSurfaceVariant" />
<View
android:layout_width="30dp"
android:layout_height="11sp"
android:layout_marginTop="@dimen/l_25"
android:background="?colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<LinearLayout
android:id="@+id/theme_primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:background="?colorPrimary"
android:orientation="vertical"
android:padding="@dimen/l1"
app:layout_constraintTop_toBottomOf="@+id/theme_card_bottom">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="125dp"
android:layout_height="wrap_content"
android:text="@{theme.themeName}"
android:textAppearance="@style/AppearanceFoundation.Title.OnPrimary"
android:textStyle="bold"
tools:text="Default" />
<View
android:layout_width="match_parent"
android:layout_height="12sp"
android:background="?colorOnPrimaryVariant" />
<View
android:layout_width="75dp"
android:layout_height="12sp"
android:layout_marginTop="@dimen/l_25"
android:background="?colorOnPrimaryVariant" />
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/theme_navigation"
style="@style/WidgetFoundation.Card.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:layout_marginBottom="@dimen/l1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/theme_primary">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="@dimen/l1">
<View
style="@style/WidgetFoundation.Image.Small"
android:background="?colorSecondary" />
<View
style="@style/WidgetFoundation.Image.Small"
android:layout_marginStart="@dimen/l1"
android:background="?colorDisabled" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.appcompat.widget.AppCompatImageView
style="@style/WidgetFoundation.Icon.OnPrimary"
gone="@{!theme.isSelected}"
android:layout_gravity="end|top"
android:layout_margin="@dimen/l_50"
android:background="@drawable/bg_selection_circle_green"
app:srcCompat="@drawable/ic_check_md2"
app:tint="#fff" />
</com.google.android.material.card.MaterialCardView>
</layout>
......@@ -9,6 +9,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:paddingTop="10dp" />
android:paddingTop="10dp"
android:textAppearance="@style/AppearanceFoundation.Caption" />
</ScrollView>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:ellipsize="middle"
android:gravity="start|center_vertical"
android:singleLine="true"
tools:text="@string/magisk"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant" />
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group
android:checkableBehavior="single"
android:id="@+id/main_group">
<item
android:id="@+id/magiskFragment"
android:icon="@drawable/ic_magisk_outline"
android:title="@string/magisk"/>
<item
android:id="@+id/superuserFragment"
android:icon="@drawable/ic_superuser"
android:title="@string/superuser"
android:visible="false"/>
<item
android:id="@+id/magiskHideFragment"
android:icon="@drawable/ic_magiskhide"
android:title="@string/magiskhide"
android:visible="false"/>
</group>
<group
android:checkableBehavior="single"
android:id="@+id/second_group">
<item
android:id="@+id/modulesFragment"
android:icon="@drawable/ic_extension"
android:title="@string/modules"
android:visible="false"/>
<item
android:id="@+id/reposFragment"
android:icon="@drawable/ic_cloud_download"
android:title="@string/downloads"/>
</group>
<group
android:checkableBehavior="single"
android:id="@+id/third_group">
<item
android:id="@+id/logFragment"
android:icon="@drawable/ic_bug_report"
android:title="@string/log" />
<item
android:id="@+id/settings"
android:icon="@drawable/ic_settings"
android:title="@string/settings"/>
</group>
</menu>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/homeFragment"
android:icon="@drawable/ic_home_md2"
android:title="@string/section_home"
tools:showAsAction="always" />
<item
android:id="@+id/superuserFragment"
android:icon="@drawable/ic_superuser_md2"
android:title="@string/section_superuser"
tools:showAsAction="always" />
<item
android:id="@+id/modulesFragment"
android:icon="@drawable/ic_module_md2"
android:title="@string/section_modules"
tools:showAsAction="always" />
</menu>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_focus_up"
android:icon="@drawable/ic_up_md2"
android:title="@string/hide_scroll_up"
app:showAsAction="ifRoom" />
</menu>
\ No newline at end of file
......@@ -3,14 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/app_search"
android:title=""
app:actionViewClass="android.widget.SearchView"
app:showAsAction="always"/>
<item
android:id="@+id/show_system"
android:title="@string/show_system_app"
android:checkable="true"/>
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings_md2"
android:title="@string/section_settings"
app:showAsAction="ifRoom" />
</menu>
\ No newline at end of file
......@@ -3,22 +3,15 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_save"
android:icon="@drawable/ic_save"
android:id="@+id/action_save"
android:icon="@drawable/ic_save_md2"
android:title="@string/menuSaveLog"
app:showAsAction="ifRoom"/>
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_clear"
android:icon="@drawable/ic_delete"
android:id="@+id/action_clear"
android:icon="@drawable/ic_delete_md2"
android:title="@string/menuClearLog"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/menu_refresh"
android:icon="@drawable/ic_refresh"
android:title="@string/menuReload"
app:showAsAction="ifRoom"/>
app:showAsAction="ifRoom" />
</menu>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_refresh"
android:icon="@drawable/ic_refresh_data_md2"
android:title="Refresh Local Data"
app:showAsAction="ifRoom" />
</menu>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/reboot"
android:title="@string/reboot"
app:showAsAction="never" />
<item
android:id="@+id/reboot_recovery"
android:title="@string/reboot_recovery"
app:showAsAction="never" />
<item
android:id="@+id/reboot_bootloader"
android:title="@string/reboot_bootloader"
app:showAsAction="never" />
<item
android:id="@+id/reboot_download"
android:title="@string/reboot_download"
app:showAsAction="never" />
<item
android:id="@+id/reboot_edl"
android:title="@string/reboot_edl"
app:showAsAction="never" />
</menu>
\ No newline at end of file
......@@ -3,15 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/repo_sort"
android:title=""
android:icon="@drawable/ic_sort"
app:showAsAction="always"/>
<item
android:id="@+id/repo_search"
android:title=""
app:actionViewClass="android.widget.SearchView"
app:showAsAction="always"/>
android:id="@+id/action_log"
android:icon="@drawable/ic_bug_md2"
android:title="@string/section_log"
app:showAsAction="ifRoom" />
</menu>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--1st party themes-->
<style name="ThemeFoundationMD2.Piplup">
<item name="colorPrimary">#4EAFF5</item>
<item name="colorPrimaryVariant">#804EAFF5</item>
<item name="colorSecondary">#3E78AF</item>
<item name="colorSecondaryVariant">#803E78AF</item>
<item name="colorSurface">#0D0D0D</item>
<item name="colorSurfaceVariant">#1F1F1F</item>
<item name="colorSurfaceSurfaceVariant">?colorSurfaceVariant</item>
<item name="colorOnPrimary">#F9F9F9</item>
<item name="colorOnPrimaryVariant">#D9E6E6E6</item>
<item name="colorOnSecondary">#F9F9F9</item>
<item name="colorOnBackground">?colorOnSurface</item>
<item name="colorError">#CC0047</item>
<item name="colorOnError">#F9F9F9</item>
<item name="colorOnSurface">#D8D8D8</item>
<item name="colorOnSurfaceVariant">#80BABABA</item>
<item name="colorDisabled">#808080</item>
<item name="colorDisabledVariant">#66808080</item>
</style>
<style name="ThemeFoundationMD2.Amoled" parent="ThemeFoundationMD2.Piplup">
<item name="colorSurface">#000</item>
<item name="colorOnPrimary">#FFF</item>
<item name="colorOnSecondary">#FFF</item>
<item name="colorOnBackground">#000</item>
<item name="colorOnError">#FFF</item>
</style>
<!--3rd party themes-->
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fab_padding">24dp</dimen>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Foundation.Compat">
<item name="android:windowTranslucentNavigation">true</item>
<item name="android:windowTranslucentStatus">true</item>
</style>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="WidgetFoundation.ProgressBar.Indeterminate" parent="Widget.AppCompat.ProgressBar.Horizontal">
<item name="android:indeterminate">true</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">100dp</item>
<item name="android:indeterminateTint">?colorPrimary</item>
</style>
<style name="WidgetFoundation.ProgressBar.Indeterminate.Circular" parent="Widget.AppCompat.ProgressBar">
<item name="android:indeterminate">true</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:indeterminateTint">?colorPrimary</item>
</style>
</resources>
\ No newline at end of file
......@@ -91,4 +91,10 @@
<item>@string/sort_by_update</item>
</string-array>
<string-array name="span_count">
<item>@string/settings_grid_span_count_1</item>
<item>@string/settings_grid_span_count_2</item>
<item>@string/settings_grid_span_count_3</item>
</string-array>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="cardStyle" format="reference"/>
<!--region Deprecated-->
<attr name="cardStyle" format="reference" />
<attr name="colorAccentFallback" format="reference" />
<attr name="imageColorTint" format="reference" />
<attr name="colorControl" format="reference"/>
<attr name="colorControl" format="reference" />
<!--endregion-->
<!--region Colors-->
<!--Static-->
<attr name="colorDisabled" format="color" />
<attr name="colorDisabledVariant" format="color" />
<attr name="colorSurfaceVariant" format="color" />
<attr name="colorOnPrimaryVariant" format="color" />
<attr name="colorOnSurfaceVariant" format="color" />
<attr name="colorSurfaceSurfaceVariant" format="color" />
<!--endregion-->
</resources>
<?xml version="1.0" encoding="utf-8"?>
<!--
Pre Android 5.0 does not support accessing colors from attributes in drawables
These values are from the default theme (Piplup light)
-->
<resources>
<color name="defColorPrimary">#4EAFF5</color>
<color name="defColorPrimaryVariant">#804EAFF5</color>
<color name="defColorSecondary">#3E78AF</color>
<color name="defColorSecondaryVariant">#803E78AF</color>
<color name="defColorSurface">#F9F9F9</color>
<color name="defColorSurfaceVariant">#E8E8E8</color>
<color name="defColorSurfaceSurfaceVariant">@color/defColorOnSurface</color>
<color name="defColorOnPrimary">#F9F9F9</color>
<color name="defColorOnPrimaryVariant">#D9E6E6E6</color>
<color name="defColorOnSecondary">#F9F9F9</color>
<color name="defColorOnBackground">@color/defColorOnSurface</color>
<color name="defColorError">#CC0047</color>
<color name="defColorOnError">#F9F9F9</color>
<color name="defColorOnSurface">#444444</color>
<color name="defColorOnSurfaceVariant">#80444444</color>
<color name="defColorDisabled">#808080</color>
<color name="defColorDisabledVariant">#66808080</color>
</resources>
......@@ -3,13 +3,22 @@
<dimen name="card_elevation">2dp</dimen>
<dimen name="fab_padding">16dp</dimen>
<!--new dimens-->
<dimen name="margin_generic_double">32dp</dimen>
<dimen name="margin_generic">16dp</dimen>
<dimen name="margin_generic_half">8dp</dimen>
<dimen name="radius_generic">8dp</dimen>
<!--new dimens ^^-->
<dimen name="l_125">2dp</dimen>
<dimen name="l_25">4dp</dimen>
<dimen name="l_50">8dp</dimen>
<dimen name="l_75">12dp</dimen>
<dimen name="l1">16dp</dimen>
<dimen name="l2">32dp</dimen>
<dimen name="r1">8dp</dimen>
<!--l1 + fab size-->
<dimen name="margin_fab">80dp</dimen>
<dimen name="internal_action_bar_size">56dp</dimen>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="hideWhenSelected" type="id" />
<item name="recyclerScrollListener" type="id" />
<item name="revealAnim" type="id" />
</resources>
\ No newline at end of file
......@@ -137,9 +137,11 @@
<string name="settings_update_channel_title">Update Channel</string>
<string name="settings_update_stable">Stable</string>
<string name="settings_update_beta">Beta</string>
<string name="settings_update_custom">Custom</string>
<string name="settings_update_custom">Custom Channel</string>
<string name="settings_update_custom_msg">Insert a custom URL</string>
<!--deprecated-->
<string name="settings_core_only_title">Magisk Core Only Mode</string>
<!--deprecated-->
<string name="settings_core_only_summary">Enable only core features. MagiskSU and MagiskHide will still be enabled, but no modules will be loaded</string>
<string name="settings_magiskhide_summary">Hide Magisk from various forms of detection</string>
<string name="settings_hosts_title">Systemless hosts</string>
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="manager">Manager</string>
<string name="system">System</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="no_connection">No connection available</string>
<string name="section_home">Home</string>
<string name="section_modules">@string/modules</string>
<string name="section_superuser">@string/superuser</string>
<string name="section_log">@string/log</string>
<string name="section_settings">@string/settings</string>
<string name="section_theme">Themes</string>
<string name="installed_error_md2">is not installed</string>
<string name="channel_error_md2">has invalid update channel!</string>
<string name="up_to_date_md2">is up to date</string>
<string name="obsolete_md2">can be updated!</string>
<string name="loading_md2">is loading…</string>
<string name="home_links_mainline">\@topjohnwu</string>
<string name="home_links_app">\@diareuse</string>
<string name="home_links_project">Project links</string>
<string name="home_item_paypal">PayPal</string>
<string name="home_item_patreon">Patreon</string>
<string name="home_item_twitter">Twitter</string>
<string name="home_item_source">Source</string>
<string name="home_item_xda">XDA</string>
<string name="home_notice_content">Always make sure you\'re using open-source Magisk Manager. Manager of unknown source can perform malicious actions.</string>
<string name="home_notice_hide">Hide</string>
<string name="home_support_title">Support Us</string>
<string name="home_support_content">Magisk is, and always will be, free and open-source. You can however show us that you care by sending a small donation.</string>
<string name="home_device_security">Security</string>
<string name="home_device_system">System</string>
<string name="home_device_build_style">A/B</string>
<string name="home_device_build_type">SAR</string>
<string name="home_device_security_secure">Secure</string>
<string name="home_device_security_outdated">Outdated</string>
<string name="home_extra_version">Version</string>
<string name="home_extra_code">Code</string>
<string name="home_extra_mode">Mode</string>
<string name="home_extra_connection">Connection</string>
<string name="home_extra_package">Package</string>
<string name="home_extra_mode_normal">Normal</string>
<string name="home_extra_mode_safe">Safe</string>
<string name="home_extra_mode_stub">Dynamic</string>
<string name="safetyNet">SafetyNet</string>
<!--ref. Magisk Hide-->
<string name="magisk_hide_md2">Hide</string>
<string name="safetynet_attest_success">Success!</string>
<string name="safetynet_attest_failure">Attestation failed!</string>
<string name="safetynet_attest_loading">Just a sec…</string>
<string name="safetynet_attest_restart">Try again</string>
<string name="settings_dark_mode_title">Theme Mode</string>
<string name="settings_dark_mode_message">Select mode which best suits your style!</string>
<string name="settings_dark_mode_light">Always Light</string>
<string name="settings_dark_mode_system">Follow System</string>
<string name="settings_dark_mode_dark">Always Dark</string>
<string name="settings_safe_mode_title">Safe Mode</string>
<string name="settings_safe_mode_summary">Disables everything but essential functionality within Magisk and Magisk Manager. Magisk Hide, as a separate subsystem, will stand unaffected.</string>
<string name="settings_grid_span_count_title">Grid Column Size</string>
<string name="settings_grid_span_count_summary">Sets column size for all eligible grid lists. You can set this outside settings by performing pinch gesture.</string>
<string name="settings_grid_span_count_1">One item per line (Small Screens)</string>
<string name="settings_grid_span_count_2">Two items per line (Recommended)</string>
<string name="settings_grid_span_count_3">Three item per line (Tablet/TV)</string>
<string name="install_options_title">Options</string>
<string name="install_method_title">Method</string>
<string name="install_next">Next</string>
<string name="install_start">Let\'s go</string>
<string name="module_safe_mode_message">You\'re in safe mode. None of user modules will work.\nThis message will disappear once safe mode is disabled.</string>
<string name="module_version_author">%1$s by %2$s</string>
<string name="module_section_pending">Updates</string>
<string name="module_section_pending_action">Update all</string>
<string name="module_section_installed">Installed</string>
<string name="module_section_remote">Remote</string>
<string name="module_state_remove">Remove</string>
<string name="module_state_restore">Restore</string>
<string name="module_action_install_external">Install from storage</string>
<string name="module_update_none">Your modules are up to date!</string>
<string name="module_install_none">No modules detected, try installing one from the list below.</string>
<string name="superuser_toggle_log">Logs</string>
<string name="superuser_toggle_notification">Notifications</string>
<string name="superuser_toggle_revoke">Revoke</string>
<string name="superuser_policy_none">No app has asked for superuser permission yet.</string>
<string name="hide_filter_hint">Filter by name</string>
<string name="hide_scroll_up">Scroll up</string>
<string name="hide_filters">Filters</string>
<string name="hide_search">Search</string>
<string name="log_data_none">You\'re log-free, try using your SU enabled apps more.</string>
<string name="log_data_magisk_none">Magisk logs are empty, that\'s weird.</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Foundation" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="android:windowBackground">?colorSurface</item>
</style>
<!--This should be overridden in v19 for transparency, etc-->
<style name="Foundation.Compat" />
<!--region Do not remove-->
<style name="WidgetFoundation" parent="android:Widget" />
<style name="AppearanceFoundation">
<item name="android:fontFamily">@font/exo</item>
</style>
<!--endregion-->
<style name="Foundation.Base" parent="Foundation.Compat">
<item name="android:fontFamily">@font/exo</item>
<item name="android:includeFontPadding">false</item>
<!--///-->
<item name="selectableItemBackground">@drawable/bg_selectable</item>
<item name="selectableItemBackgroundBorderless">@drawable/bg_selectable_borderless</item>
<item name="actionBarSize">@dimen/internal_action_bar_size</item>
</style>
<style name="Foundation.Default" parent="Foundation.Base">
<item name="android:windowBackground">?colorSurface</item>
</style>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--region Display-->
<style name="AppearanceFoundation.Display" parent="TextAppearance.AppCompat.Display1">
<item name="android:textColor">?attr/colorOnSurface</item>
</style>
<style name="AppearanceFoundation.Display.Secondary">
<item name="android:textColor">?colorSecondary</item>
</style>
<style name="AppearanceFoundation.Display.Variant">
<item name="android:textColor">?attr/colorOnSurfaceVariant</item>
</style>
<style name="AppearanceFoundation.Display.OnPrimary">
<item name="android:textColor">?attr/colorOnPrimary</item>
</style>
<style name="AppearanceFoundation.Display.OnPrimary.Variant">
<item name="android:textColor">?attr/colorOnPrimaryVariant</item>
</style>
<!--endregion-->
<!--region Title-->
<style name="AppearanceFoundation.Title" parent="TextAppearance.AppCompat.Title">
<item name="android:textColor">?attr/colorOnSurface</item>
<item name="android:textStyle">bold</item>
<item name="android:fontFamily">@font/exo_bold</item>
</style>
<style name="AppearanceFoundation.Title.Variant">
<item name="android:textColor">?attr/colorOnSurfaceVariant</item>
</style>
<style name="AppearanceFoundation.Title.OnPrimary">
<item name="android:textColor">?attr/colorOnPrimary</item>
</style>
<style name="AppearanceFoundation.Title.OnPrimary.Variant">
<item name="android:textColor">?attr/colorOnPrimaryVariant</item>
</style>
<!--endregion-->
<!--region Body-->
<style name="AppearanceFoundation.Body" parent="TextAppearance.AppCompat.Body1">
<item name="android:textColor">?attr/colorOnSurface</item>
</style>
<style name="AppearanceFoundation.Body.Variant">
<item name="android:textColor">?attr/colorOnSurfaceVariant</item>
</style>
<style name="AppearanceFoundation.Body.Primary">
<item name="android:textColor">?attr/colorPrimary</item>
</style>
<style name="AppearanceFoundation.Body.Secondary">
<item name="android:textColor">?attr/colorSecondary</item>
</style>
<style name="AppearanceFoundation.Body.OnPrimary">
<item name="android:textColor">?attr/colorOnPrimary</item>
</style>
<style name="AppearanceFoundation.Body.OnPrimary.Variant">
<item name="android:textColor">?attr/colorOnPrimaryVariant</item>
</style>
<!--endregion-->
<!--region Caption-->
<style name="AppearanceFoundation.Caption" parent="TextAppearance.AppCompat.Caption">
<item name="android:textColor">?attr/colorOnSurface</item>
</style>
<style name="AppearanceFoundation.Caption.Variant">
<item name="android:textColor">?attr/colorOnSurfaceVariant</item>
</style>
<style name="AppearanceFoundation.Caption.Primary">
<item name="android:textColor">?attr/colorPrimary</item>
</style>
<style name="AppearanceFoundation.Caption.Secondary">
<item name="android:textColor">?attr/colorSecondary</item>
</style>
<style name="AppearanceFoundation.Caption.OnPrimary">
<item name="android:textColor">?attr/colorOnPrimary</item>
</style>
<style name="AppearanceFoundation.Caption.OnPrimary.Variant">
<item name="android:textColor">?attr/colorOnPrimaryVariant</item>
</style>
<!--endregion-->
<!--region Tiny-->
<style name="AppearanceFoundation.Tiny" parent="TextAppearance.AppCompat.Caption">
<item name="android:textColor">?attr/colorOnSurface</item>
<item name="android:textSize">11sp</item>
</style>
<style name="AppearanceFoundation.Tiny.Bold">
<item name="android:textStyle">bold</item>
</style>
<style name="AppearanceFoundation.Tiny.Variant">
<item name="android:textColor">?attr/colorOnSurfaceVariant</item>
</style>
<style name="AppearanceFoundation.Tiny.OnPrimary">
<item name="android:textColor">?attr/colorOnPrimary</item>
</style>
<style name="AppearanceFoundation.Tiny.OnPrimary.Variant">
<item name="android:textColor">?attr/colorOnPrimaryVariant</item>
</style>
<!--endregion-->
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="WidgetFoundation.Appbar" parent="Widget.MaterialComponents.AppBarLayout.Surface">
<item name="elevation">0dp</item>
<item name="android:background">@drawable/bg_appbar</item>
</style>
<style name="WidgetFoundation.Toolbar" parent="Widget.MaterialComponents.Toolbar.Surface">
<item name="titleTextAppearance">@style/AppearanceFoundation.Title</item>
<item name="titleTextColor">?colorOnSurface</item>
<item name="subtitleTextAppearance">@style/AppearanceFoundation.Caption</item>
<item name="subtitleTextColor">?colorOnSurfaceVariant</item>
<item name="android:background">@android:color/transparent</item>
<item name="contentInsetStartWithNavigation">0dp</item>
<item name="android:layout_height">?actionBarSize</item>
</style>
<style name="WidgetFoundation.Card" parent="Widget.MaterialComponents.CardView">
<item name="cardBackgroundColor">?colorSurfaceVariant</item>
<item name="cardCornerRadius">@dimen/l_50</item>
<item name="cardElevation">0dp</item>
<item name="cardPreventCornerOverlap">true</item>
</style>
<style name="WidgetFoundation.Card.Primary">
<item name="cardBackgroundColor">?colorPrimary</item>
</style>
<style name="WidgetFoundation.Card.Secondary">
<item name="cardBackgroundColor">?colorSecondary</item>
</style>
<style name="WidgetFoundation.Card.Variant">
<item name="cardBackgroundColor">?colorSurface</item>
<item name="strokeColor">?colorSurfaceVariant</item>
<item name="strokeWidth">1.5dp</item>
</style>
<style name="WidgetFoundation.Card.Variant.Primary">
<item name="strokeColor">?colorPrimary</item>
</style>
<style name="WidgetFoundation.Card.Elevated" parent="Widget.MaterialComponents.CardView">
<item name="cardBackgroundColor">?colorSurfaceSurfaceVariant</item>
<item name="cardCornerRadius">@dimen/l_50</item>
<item name="cardElevation">@dimen/l_125</item>
<item name="cardPreventCornerOverlap">true</item>
</style>
<style name="WidgetFoundation.Card.OnPrimary" parent="Widget.MaterialComponents.CardView">
<item name="cardCornerRadius">@dimen/l_50</item>
<item name="cardElevation">0dp</item>
<item name="strokeColor">?colorOnPrimaryVariant</item>
<item name="strokeWidth">1dp</item>
<item name="cardBackgroundColor">?colorPrimary</item>
<item name="cardPreventCornerOverlap">true</item>
</style>
<style name="WidgetFoundation.Button" parent="Widget.MaterialComponents.Button">
<item name="android:textStyle">bold</item>
<item name="iconGravity">textStart</item>
<item name="iconPadding">@dimen/l_50</item>
</style>
<style name="WidgetFoundation.Button.OnPrimary">
<item name="backgroundTint">?colorOnPrimary</item>
<item name="rippleColor">?colorPrimary</item>
<item name="android:textColor">?colorPrimary</item>
<item name="iconTint">?colorPrimary</item>
</style>
<style name="WidgetFoundation.Button.Outlined" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="android:textStyle">bold</item>
<item name="iconGravity">textStart</item>
<item name="iconPadding">@dimen/l_50</item>
<item name="strokeColor">?colorPrimary</item>
<item name="android:textColor">?colorPrimary</item>
</style>
<style name="WidgetFoundation.Button.Outlined.OnPrimary">
<item name="strokeColor">?colorOnPrimary</item>
<item name="rippleColor">?colorOnPrimary</item>
<item name="android:textColor">?colorOnPrimary</item>
<item name="iconTint">?colorOnPrimary</item>
</style>
<style name="WidgetFoundation.Button.Flat" parent="Widget.MaterialComponents.Button.UnelevatedButton">
<item name="android:textStyle">bold</item>
<item name="iconGravity">textStart</item>
<item name="iconPadding">@dimen/l_50</item>
</style>
<style name="WidgetFoundation.Button.Flat.OnPrimary">
<item name="backgroundTint">?colorOnPrimary</item>
<item name="rippleColor">?colorPrimary</item>
<item name="android:textColor">?colorPrimary</item>
<item name="iconTint">?colorPrimary</item>
</style>
<style name="WidgetFoundation.Button.Text" parent="Widget.MaterialComponents.Button.TextButton">
<item name="android:textStyle">bold</item>
<item name="iconGravity">textStart</item>
<item name="iconPadding">@dimen/l_50</item>
</style>
<style name="WidgetFoundation.Button.Text.Secondary">
<item name="android:textColor">?colorSecondary</item>
<item name="iconTint">?colorSecondary</item>
</style>
<style name="WidgetFoundation.Button.Text.OnPrimary">
<item name="rippleColor">?colorOnPrimary</item>
<item name="android:textColor">?colorOnPrimary</item>
<item name="iconTint">?colorOnPrimary</item>
</style>
<style name="WidgetFoundation.Button.Error" parent="WidgetFoundation.Button.Text">
<item name="rippleColor">?colorError</item>
<item name="android:textColor">?colorError</item>
<item name="iconTint">?colorError</item>
</style>
<style name="WidgetFoundation.Image">
<item name="android:layout_width">32dp</item>
<item name="android:layout_height">32dp</item>
</style>
<style name="WidgetFoundation.Image.Big">
<item name="android:layout_width">48dp</item>
<item name="android:layout_height">48dp</item>
</style>
<style name="WidgetFoundation.Image.Small">
<item name="android:layout_width">24dp</item>
<item name="android:layout_height">24dp</item>
</style>
<style name="WidgetFoundation.Icon" parent="WidgetFoundation.Image.Big">
<item name="android:padding">@dimen/l_75</item>
<item name="android:background">?selectableItemBackgroundBorderless</item>
<item name="tint">@color/color_text_transient</item>
</style>
<style name="WidgetFoundation.Icon.Primary">
<item name="tint">@color/color_primary_transient</item>
</style>
<style name="WidgetFoundation.Icon.Secondary">
<item name="tint">@color/color_secondary_transient</item>
</style>
<style name="WidgetFoundation.Icon.OnPrimary">
<item name="tint">@color/color_on_primary_transient</item>
</style>
<style name="WidgetFoundation.Icon.Error">
<item name="tint">@color/color_error_transient</item>
</style>
<style name="WidgetFoundation.Checkbox" parent="Widget.AppCompat.CompoundButton.CheckBox">
<item name="android:textColor">?attr/colorOnSurface</item>
<item name="android:textAppearance">@style/AppearanceFoundation.Body</item>
<item name="android:paddingStart">@dimen/l1</item>
<item name="android:paddingEnd">@dimen/l1</item>
<item name="tint">?colorPrimary</item>
<item name="buttonCompat">@drawable/ic_radio_check_button</item>
</style>
<style name="WidgetFoundation.RadioButton" parent="Widget.AppCompat.CompoundButton.RadioButton">
<item name="android:textColor">?attr/colorOnSurface</item>
<item name="android:textAppearance">@style/AppearanceFoundation.Body</item>
<item name="android:paddingStart">@dimen/l1</item>
<item name="android:paddingEnd">@dimen/l1</item>
<item name="tint">?colorPrimary</item>
<item name="buttonCompat">@drawable/ic_radio_check_button</item>
</style>
<style name="WidgetFoundation.ProgressBar" parent="Widget.AppCompat.ProgressBar.Horizontal">
<item name="android:indeterminate">false</item>
<item name="android:layout_height">4dp</item>
<item name="android:progressDrawable">@drawable/progress_determinate</item>
</style>
<style name="WidgetFoundation.ProgressBar.Indeterminate" parent="Widget.AppCompat.ProgressBar.Horizontal">
<item name="android:indeterminate">true</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">100dp</item>
</style>
<style name="WidgetFoundation.ProgressBar.Indeterminate.Circular" parent="Widget.AppCompat.ProgressBar">
<item name="android:indeterminate">true</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">wrap_content</item>
</style>
<style name="WidgetFoundation.Switch" parent="Widget.AppCompat.CompoundButton.Switch">
<item name="thumbTint">@color/color_checkbox_primary</item>
</style>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="W" parent="Widget" />
<style name="W.Home" />
<style name="W.Home.Extra" />
<style name="W.Home.Extra.Title">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textAppearance">@style/AppearanceFoundation.Caption.Variant</item>
<item name="android:singleLine">true</item>
</style>
<style name="W.Home.Extra.Value">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textAppearance">@style/AppearanceFoundation.Caption</item>
<item name="android:textStyle">bold</item>
<item name="android:layout_marginStart">@dimen/l_50</item>
<item name="layout_constrainedWidth">true</item>
<item name="layout_constraintHorizontal_bias">0</item>
<item name="android:ellipsize">middle</item>
<item name="android:singleLine">true</item>
</style>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
# Theme guide
This should provide all the information you need to create new !color! themes.
## Inheritance
You might want to inherit default values from "base" theme. You can do so implicitly by just
writing `<style name="ThemeFoundationMD2.MySuperAwesomeTheme" parent="ThemeFoundationMD2.Piplup">`.
With this approach you can only change values you (don't) like - such as `colorPrimary` or
`colorSecondary`.
## Day / Night ? How do I define both in one theme?
Define Theme here for "Day" theme and in `values-night` directory define theme with same name
and parent if applicable. The framework will automatically switch between day/night themes based
on user's requested configuration. (Always light, Always dark, Follow system)
You might choose to define only "Day" theme with Dark colors to have dark theme regardless.
That's super lazy approach and is discouraged but the framework permits it.
## What to theme
Generally I'd suggest theming only `colorPrimary`, `colorSecondary` and their variants. Make
sure that text is readable if displayed on primary or secondary color!
You can check that very easily by visiting https://www.colorhexa.com/. After you put in your
color hex, you can scroll down to "Preview" where it will compute whether dark/bright text
should be displayed on top of it. Then you must edit respective `colorOn...`.
Also check "Color Blindness Simulator". Primary and secondary colors must not match or be
similar in type. Text on color must have enough contrast so it can be read by everyone!
!! Themes that will not satisfy these requirements will be promptly deleted without further
!! notice. In repeated attempts to push such themes you will be automatically blacklisted.
-->
<style name="ThemeFoundationMD2" parent="Foundation.Default" />
<!--1st party themes-->
<style name="ThemeFoundationMD2.Piplup">
<item name="colorPrimary">#4EAFF5</item>
<item name="colorPrimaryVariant">#804EAFF5</item>
<item name="colorSecondary">#3E78AF</item>
<item name="colorSecondaryVariant">#803E78AF</item>
<item name="colorSurface">#F9F9F9</item>
<item name="colorSurfaceVariant">#E8E8E8</item>
<item name="colorSurfaceSurfaceVariant">?colorSurface</item>
<item name="colorOnPrimary">#F9F9F9</item>
<item name="colorOnPrimaryVariant">#D9E6E6E6</item>
<item name="colorOnSecondary">#F9F9F9</item>
<item name="colorOnBackground">?colorOnSurface</item>
<item name="colorError">#CC0047</item>
<item name="colorOnError">#F9F9F9</item>
<item name="colorOnSurface">#444444</item>
<item name="colorOnSurfaceVariant">#80444444</item>
<item name="colorDisabled">#808080</item>
<item name="colorDisabledVariant">#66808080</item>
</style>
<style name="ThemeFoundationMD2.Amoled" parent="ThemeFoundationMD2.Piplup">
<item name="colorSurface">#FFF</item>
<item name="colorOnPrimary">#FFF</item>
<item name="colorOnSecondary">#FFF</item>
<item name="colorOnBackground">#FFF</item>
<item name="colorOnError">#FFF</item>
</style>
<style name="ThemeFoundationMD2.Rayquaza" parent="ThemeFoundationMD2.Piplup">
<item name="colorPrimary">#68A17F</item>
<item name="colorPrimaryVariant">#8068A17F</item>
<item name="colorSecondary">#2F6D43</item>
<item name="colorSecondaryVariant">#802F6D43</item>
</style>
<style name="ThemeFoundationMD2.Zapdos" parent="ThemeFoundationMD2.Piplup">
<item name="colorPrimary">#FBD179</item>
<item name="colorPrimaryVariant">#80FBD179</item>
<item name="colorSecondary">#B29667</item>
<item name="colorSecondaryVariant">#80B29667</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorOnPrimaryVariant">#D9222222</item>
</style>
<style name="ThemeFoundationMD2.Charmeleon" parent="ThemeFoundationMD2.Piplup">
<item name="colorPrimary">#DB7366</item>
<item name="colorPrimaryVariant">#80DB7366</item>
<item name="colorSecondary">#B65247</item>
<item name="colorSecondaryVariant">#80B65247</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorOnPrimaryVariant">#D9222222</item>
</style>
<style name="ThemeFoundationMD2.Mew" parent="ThemeFoundationMD2.Piplup">
<item name="colorPrimary">#D9ADB7</item>
<item name="colorPrimaryVariant">#80E9BBC5</item>
<item name="colorSecondary">#B5889B</item>
<item name="colorSecondaryVariant">#80B5889B</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorOnPrimaryVariant">#D9222222</item>
</style>
<style name="ThemeFoundationMD2.Salamence" parent="ThemeFoundationMD2.Piplup">
<item name="colorPrimary">#70B2C6</item>
<item name="colorPrimaryVariant">#8070B2C6</item>
<item name="colorSecondary">#C06A75</item>
<item name="colorSecondaryVariant">#80C06A75</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorOnPrimaryVariant">#D9222222</item>
</style>
<!--3rd party themes-->
</resources>
\ No newline at end of file
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:key="general"
android:title="@string/settings_general_category">
......@@ -131,4 +132,15 @@
</PreferenceCategory>
<PreferenceCategory
android:key="redesign_cat"
app:title="Redesign">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="redesign"
app:title="Enable Redesign" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
......@@ -16,7 +16,7 @@ buildscript {
maven { url 'https://kotlin.bintray.com/kotlinx' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.android.tools.build:gradle:3.6.0-rc01'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${vKotlin}"
......
#Sun Jan 12 04:46:30 CST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment