Commit c1dad11c authored by topjohnwu's avatar topjohnwu

Merge branch 'md2'

parents 12b219e7 12a9792c
...@@ -17,3 +17,4 @@ tools/** binary ...@@ -17,3 +17,4 @@ tools/** binary
*.apk binary *.apk binary
*.png binary *.png binary
*.jpg binary *.jpg binary
*.ttf binary
...@@ -19,6 +19,12 @@ android { ...@@ -19,6 +19,12 @@ android {
multiDexEnabled true multiDexEnabled true
versionName props['appVersion'] versionName props['appVersion']
versionCode props['appVersionCode'] as Integer versionCode props['appVersionCode'] as Integer
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.incremental":"true"]
}
}
} }
buildTypes { buildTypes {
...@@ -62,7 +68,7 @@ dependencies { ...@@ -62,7 +68,7 @@ dependencies {
implementation 'com.ncapdevi:frag-nav:3.2.0' implementation 'com.ncapdevi:frag-nav:3.2.0'
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6' 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:rxkotlin:2.4.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
...@@ -89,14 +95,16 @@ dependencies { ...@@ -89,14 +95,16 @@ dependencies {
implementation "org.koin:koin-android:${vKoin}" implementation "org.koin:koin-android:${vKoin}"
implementation "org.koin:koin-androidx-viewmodel:${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:retrofit:${vRetrofit}"
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}" implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}" implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}" implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
def vOkHttp = '3.12.6' def vOkHttp = '3.12.7'
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}" implementation("com.squareup.okhttp3:okhttp:${vOkHttp}") {
force = true
}
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}" implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
def vMoshi = '1.9.2' def vMoshi = '1.9.2'
...@@ -111,7 +119,7 @@ dependencies { ...@@ -111,7 +119,7 @@ dependencies {
replacedBy('com.github.topjohnwu:room-runtime') replacedBy('com.github.topjohnwu:room-runtime')
} }
} }
def vRoom = '2.2.2' def vRoom = '2.2.3'
implementation "com.github.topjohnwu:room-runtime:${vRoom}" implementation "com.github.topjohnwu:room-runtime:${vRoom}"
implementation "androidx.room:room-rxjava2:${vRoom}" implementation "androidx.room:room-rxjava2:${vRoom}"
kapt "androidx.room:room-compiler:${vRoom}" kapt "androidx.room:room-compiler:${vRoom}"
...@@ -120,16 +128,16 @@ dependencies { ...@@ -120,16 +128,16 @@ dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:${vNav}" implementation "androidx.navigation:navigation-fragment-ktx:${vNav}"
implementation "androidx.navigation:navigation-ui-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.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.fragment:fragment-ktx:1.2.0-rc03' implementation 'androidx.fragment:fragment-ktx:1.2.0-rc05'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.work:work-runtime:2.2.0' implementation 'androidx.work:work-runtime:2.2.0'
implementation 'androidx.transition:transition:1.3.0-rc02' implementation 'androidx.transition:transition:1.3.0-rc02'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.core:core-ktx:1.1.0' implementation 'androidx.core:core-ktx:1.2.0-rc01'
implementation 'com.google.android.material:material:1.2.0-alpha02' implementation 'com.google.android.material:material:1.2.0-alpha03'
} }
...@@ -17,32 +17,26 @@ ...@@ -17,32 +17,26 @@
#} #}
# Snet # Snet
-keepclassmembers class com.topjohnwu.magisk.utils.SafetyNetHelper { *; } -keepclassmembers class com.topjohnwu.magisk.core.utils.SafetyNetHelper { *; }
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.SafetyNetHelper$Callback -keep,allowobfuscation interface com.topjohnwu.magisk.core.utils.SafetyNetHelper$Callback
-keepclassmembers class * implements com.topjohnwu.magisk.utils.SafetyNetHelper$Callback { -keepclassmembers class * implements com.topjohnwu.magisk.core.utils.SafetyNetHelper$Callback {
void onResponse(int); void onResponse(int);
} }
# Keep all fragment constructors # Fragments
-keepclassmembers class * extends androidx.fragment.app.Fragment { -keep,allowobfuscation class * extends androidx.fragment.app.Fragment
public <init>(...);
}
# DelegateWorker # BaseWorkerWrapper
-keep,allowobfuscation class * extends com.topjohnwu.magisk.base.DelegateWorker -keep,allowobfuscation class * extends com.topjohnwu.magisk.core.base.BaseWorkerWrapper
# BootSigner # BootSigner
-keep class a.a { *; } -keep class a.a { *; }
# Workaround R8 bug
-keep,allowobfuscation class com.topjohnwu.magisk.model.receiver.GeneralReceiver
-keepclassmembers class a.e { *; }
# Strip logging # Strip logging
-assumenosideeffects class timber.log.Timber.Tree { *; } -assumenosideeffects class timber.log.Timber.Tree { *; }
# Excessive obfuscation # Excessive obfuscation
-repackageclasses 'a' -repackageclasses a
-allowaccessmodification -allowaccessmodification
# QOL # QOL
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.topjohnwu.magisk"> package="com.topjohnwu.magisk">
...@@ -21,6 +20,10 @@ ...@@ -21,6 +20,10 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity> </activity>
<!-- Main --> <!-- Main -->
......
package a; package a;
import com.topjohnwu.magisk.utils.PatchAPK; import com.topjohnwu.magisk.core.utils.PatchAPK;
import com.topjohnwu.signing.BootSigner; import com.topjohnwu.signing.BootSigner;
public class a { 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; ...@@ -2,11 +2,11 @@ package a;
import android.content.Context; import android.content.Context;
import com.topjohnwu.magisk.model.update.UpdateCheckService;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.work.WorkerParameters; import androidx.work.WorkerParameters;
import com.topjohnwu.magisk.core.UpdateCheckService;
public class g extends w<UpdateCheckService> { public class g extends w<UpdateCheckService> {
/* Stub */ /* Stub */
public g(@NonNull Context context, @NonNull WorkerParameters workerParams) { 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.app.Application
import android.content.Context import android.content.Context
...@@ -9,6 +9,12 @@ import androidx.room.Room ...@@ -9,6 +9,12 @@ import androidx.room.Room
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.impl.WorkDatabase import androidx.work.impl.WorkDatabase
import androidx.work.impl.WorkDatabase_Impl 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
import com.topjohnwu.magisk.data.database.RepoDatabase_Impl import com.topjohnwu.magisk.data.database.RepoDatabase_Impl
import com.topjohnwu.magisk.data.database.SuLogDatabase import com.topjohnwu.magisk.data.database.SuLogDatabase
...@@ -17,13 +23,11 @@ import com.topjohnwu.magisk.di.ActivityTracker ...@@ -17,13 +23,11 @@ import com.topjohnwu.magisk.di.ActivityTracker
import com.topjohnwu.magisk.di.koinModules import com.topjohnwu.magisk.di.koinModules
import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.unwrap 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 com.topjohnwu.superuser.Shell
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import timber.log.Timber import timber.log.Timber
import kotlin.system.exitProcess
open class App() : Application() { open class App() : Application() {
...@@ -37,7 +41,7 @@ open class App() : Application() { ...@@ -37,7 +41,7 @@ open class App() : Application() {
Shell.Config.verboseLogging(BuildConfig.DEBUG) Shell.Config.verboseLogging(BuildConfig.DEBUG)
Shell.Config.addInitializers(RootInit::class.java) Shell.Config.addInitializers(RootInit::class.java)
Shell.Config.setTimeout(2) Shell.Config.setTimeout(2)
FileProvider.callHandler = SuHandler FileProvider.callHandler = SuCallbackHandler
Room.setFactory { Room.setFactory {
when (it) { when (it) {
WorkDatabase::class.java -> WorkDatabase_Impl() WorkDatabase::class.java -> WorkDatabase_Impl()
...@@ -46,13 +50,19 @@ open class App() : Application() { ...@@ -46,13 +50,19 @@ open class App() : Application() {
else -> null 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) { override fun attachBaseContext(base: Context) {
// Basic setup // Basic setup
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
MultiDex.install(base) MultiDex.install(base)
Timber.plant(Timber.DebugTree())
// Some context magic // Some context magic
val app: Application val app: Application
......
package com.topjohnwu.magisk package com.topjohnwu.magisk.core
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Environment import android.os.Environment
import android.util.Xml import android.util.Xml
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit import androidx.core.content.edit
import com.topjohnwu.magisk.data.database.SettingsDao import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.data.database.StringDao 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.data.repository.DBConfig
import com.topjohnwu.magisk.di.Protected import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.model.preference.PreferenceModel import com.topjohnwu.magisk.model.preference.PreferenceModel
import com.topjohnwu.magisk.utils.BiometricHelper import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.core.utils.BiometricHelper
import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile import com.topjohnwu.superuser.io.SuFile
import com.topjohnwu.superuser.io.SuFileInputStream import com.topjohnwu.superuser.io.SuFileInputStream
...@@ -45,10 +48,15 @@ object Config : PreferenceModel, DBConfig { ...@@ -45,10 +48,15 @@ object Config : PreferenceModel, DBConfig {
const val CUSTOM_CHANNEL = "custom_channel" const val CUSTOM_CHANNEL = "custom_channel"
const val LOCALE = "locale" const val LOCALE = "locale"
const val DARK_THEME = "dark_theme" const val DARK_THEME = "dark_theme"
const val DARK_THEME_EXTENDED = "dark_theme_extended"
const val REPO_ORDER = "repo_order" const val REPO_ORDER = "repo_order"
const val SHOW_SYSTEM_APP = "show_system" const val SHOW_SYSTEM_APP = "show_system"
const val DOWNLOAD_PATH = "download_path" 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 BOOT_ID = "boot_id"
const val LIST_SPAN_COUNT = "list_span_count"
// system state // system state
const val MAGISKHIDE = "magiskhide" const val MAGISKHIDE = "magiskhide"
...@@ -103,38 +111,66 @@ object Config : PreferenceModel, DBConfig { ...@@ -103,38 +111,66 @@ object Config : PreferenceModel, DBConfig {
Value.CANARY_DEBUG_CHANNEL Value.CANARY_DEBUG_CHANNEL
else else
Value.CANARY_CHANNEL Value.CANARY_CHANNEL
} } else Value.DEFAULT_CHANNEL
else Value.DEFAULT_CHANNEL
var bootId by preference(Key.BOOT_ID, "") var bootId by preference(Key.BOOT_ID, "")
var downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS) 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 suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT) var suAutoReponse by preferenceStrInt(
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST) Key.SU_AUTO_RESPONSE,
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel) 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 suReAuth by preference(Key.SU_REAUTH, false)
var checkUpdate by preference(Key.CHECK_UPDATES, true) var checkUpdate by preference(Key.CHECK_UPDATES, true)
var magiskHide by preference(Key.MAGISKHIDE, true) var magiskHide by preference(Key.MAGISKHIDE, true)
@JvmStatic
var coreOnly by preference(Key.COREONLY, false) var coreOnly by preference(Key.COREONLY, false)
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, 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 customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
var locale by preference(Key.LOCALE, "") var locale by preference(Key.LOCALE, "")
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB) var rootMode by dbSettings(
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER) Key.ROOT_ACCESS,
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY) 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 suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
var suManager by dbStrings(Key.SU_MANAGER, "", true) var suManager by dbStrings(Key.SU_MANAGER, "", true)
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true) var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
// Always return a path in external storage where we can write // Always return a path in external storage where we can write
val downloadDirectory get() = val downloadDirectory
get() =
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!! Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
private const val SU_FINGERPRINT = "su_fingerprint" private const val SU_FINGERPRINT = "su_fingerprint"
...@@ -163,7 +199,9 @@ object Config : PreferenceModel, DBConfig { ...@@ -163,7 +199,9 @@ object Config : PreferenceModel, DBConfig {
} }
private fun parsePrefs(editor: SharedPreferences.Editor) = editor.apply { 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 { if (config.exists()) runCatching {
val input = SuFileInputStream(config) val input = SuFileInputStream(config)
val parser = Xml.newPullParser() val parser = Xml.newPullParser()
......
package com.topjohnwu.magisk package com.topjohnwu.magisk.core
import android.os.Process import android.os.Process
import java.io.File import java.io.File
...@@ -62,6 +62,7 @@ object Const { ...@@ -62,6 +62,7 @@ object Const {
const val ETAG_KEY = "ETag" const val ETAG_KEY = "ETag"
// intents // intents
const val OPEN_SECTION = "section" const val OPEN_SECTION = "section"
const val OPEN_SETTINGS = "settings"
const val INTENT_SET_APP = "app_json" const val INTENT_SET_APP = "app_json"
const val FLASH_ACTION = "action" const val FLASH_ACTION = "action"
const val FLASH_DATA = "additional_data" const val FLASH_DATA = "additional_data"
......
package com.topjohnwu.magisk.model.receiver package com.topjohnwu.magisk.core
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.core.base.BaseReceiver
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.Info import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.base.BaseReceiver import com.topjohnwu.magisk.core.model.ManagerJson
import com.topjohnwu.magisk.data.database.PolicyDao import com.topjohnwu.magisk.core.su.SuCallbackHandler
import com.topjohnwu.magisk.core.view.Shortcuts
import com.topjohnwu.magisk.extensions.reboot 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.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject 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 com.topjohnwu.superuser.Shell
import org.koin.core.inject import org.koin.core.inject
...@@ -30,7 +27,7 @@ open class GeneralReceiver : BaseReceiver() { ...@@ -30,7 +27,7 @@ open class GeneralReceiver : BaseReceiver() {
when (intent.action ?: return) { when (intent.action ?: return) {
Intent.ACTION_REBOOT -> { Intent.ACTION_REBOOT -> {
SuHandler(context, intent.getStringExtra("action"), intent.extras) SuCallbackHandler(context, intent.getStringExtra("action"), intent.extras)
} }
Intent.ACTION_PACKAGE_REPLACED -> { Intent.ACTION_PACKAGE_REPLACED -> {
// This will only work pre-O // This will only work pre-O
......
@file:Suppress("DEPRECATION") @file:Suppress("DEPRECATION")
package com.topjohnwu.magisk package com.topjohnwu.magisk.core
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.job.JobInfo import android.app.job.JobInfo
...@@ -14,23 +14,22 @@ import android.content.res.AssetManager ...@@ -14,23 +14,22 @@ import android.content.res.AssetManager
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import androidx.annotation.RequiresApi 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.extensions.forceGetDeclaredField
import com.topjohnwu.magisk.model.download.DownloadService import com.topjohnwu.magisk.legacy.flash.FlashActivity
import com.topjohnwu.magisk.model.receiver.GeneralReceiver import com.topjohnwu.magisk.legacy.surequest.SuRequestActivity
import com.topjohnwu.magisk.model.update.UpdateCheckService
import com.topjohnwu.magisk.ui.MainActivity 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) { fun AssetManager.addAssetPath(path: String) {
DynAPK.addAssetPath(this, path) DynAPK.addAssetPath(this, path)
} }
fun Context.wrap(global: Boolean = true): Context fun Context.wrap(global: Boolean = true): Context =
= if (global) GlobalResContext(this) else ResContext(this) if (global) GlobalResContext(this) else ResContext(this)
fun Context.wrapJob(): Context = object : GlobalResContext(this) { fun Context.wrapJob(): Context = object : GlobalResContext(this) {
...@@ -130,7 +129,8 @@ private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler ...@@ -130,7 +129,8 @@ private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler
val name = service.className val name = service.className
val component = ComponentName( val component = ComponentName(
service.packageName, service.packageName,
Info.stub!!.classToComponent[name] ?: name) Info.stub!!.classToComponent[name] ?: name
)
javaClass.forceGetDeclaredField("service")?.set(this, component) javaClass.forceGetDeclaredField("service")?.set(this, component)
return this return this
......
package com.topjohnwu.magisk package com.topjohnwu.magisk.core
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork 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.get
import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.UpdateInfo
import com.topjohnwu.magisk.utils.CachedValue import com.topjohnwu.magisk.utils.CachedValue
import com.topjohnwu.magisk.utils.KObservableField import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
...@@ -17,12 +18,16 @@ object Info { ...@@ -17,12 +18,16 @@ object Info {
val envRef = CachedValue { loadState() } val envRef = CachedValue { loadState() }
@JvmStatic
val env by envRef // Local val env by envRef // Local
var remote = UpdateInfo() // Remote var remote = UpdateInfo() // Remote
var stub: DynAPK.Data? = null // Stub var stub: DynAPK.Data? = null // Stub
@JvmStatic
var keepVerity = false var keepVerity = false
@JvmStatic
var keepEnc = false var keepEnc = false
@JvmStatic
var recovery = false var recovery = false
val isConnected by lazy { val isConnected by lazy {
......
package com.topjohnwu.magisk.ui package com.topjohnwu.magisk.core
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import com.topjohnwu.magisk.* import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.core.view.Notifications
import com.topjohnwu.magisk.core.view.Shortcuts
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
...@@ -61,8 +62,7 @@ open class SplashActivity : Activity() { ...@@ -61,8 +62,7 @@ open class SplashActivity : Activity() {
} }
DONE = true DONE = true
Navigation.start(intent, this)
startActivity(intent<MainActivity>().apply { intent?.also { putExtras(it) } })
finish() finish()
} }
......
package com.topjohnwu.magisk.model.update package com.topjohnwu.magisk.core
import androidx.work.ListenableWorker import androidx.work.ListenableWorker
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Info import com.topjohnwu.magisk.core.base.BaseWorkerWrapper
import com.topjohnwu.magisk.base.DelegateWorker import com.topjohnwu.magisk.core.view.Notifications
import com.topjohnwu.magisk.data.repository.MagiskRepository import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
class UpdateCheckService : DelegateWorker() { class UpdateCheckService : BaseWorkerWrapper() {
private val magiskRepo: MagiskRepository by inject() private val magiskRepo: MagiskRepository by inject()
......
package com.topjohnwu.magisk.base package com.topjohnwu.magisk.core.base
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.SparseArrayCompat import androidx.collection.SparseArrayCompat
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil import com.topjohnwu.magisk.core.utils.currentLocale
import androidx.databinding.ViewDataBinding import com.topjohnwu.magisk.core.wrap
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.extensions.set import com.topjohnwu.magisk.extensions.set
import com.topjohnwu.magisk.model.events.EventHandler
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
import com.topjohnwu.magisk.utils.currentLocale
import com.topjohnwu.magisk.wrap
import kotlin.random.Random import kotlin.random.Random
typealias RequestCallback = BaseActivity<*, *>.(Int, Intent?) -> Unit typealias RequestCallback = BaseActivity.(Int, Intent?) -> Unit
abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding> : abstract class BaseActivity : AppCompatActivity() {
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
private val resultCallbacks by lazy { SparseArrayCompat<RequestCallback>() } 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?) { override fun applyOverrideConfiguration(config: Configuration?) {
// Force applying our preferred local // Force applying our preferred local
config?.setLocale(currentLocale) config?.setLocale(currentLocale)
...@@ -56,18 +31,6 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding ...@@ -56,18 +31,6 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding
super.attachBaseContext(base.wrap(false)) 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) { fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
val request = PermissionRequestBuilder().apply(builder).build() val request = PermissionRequestBuilder().apply(builder).build()
val ungranted = permissions.filter { val ungranted = permissions.filter {
...@@ -101,18 +64,18 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding ...@@ -101,18 +64,18 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding
break break
} }
} }
resultCallbacks[requestCode]?.apply { resultCallbacks[requestCode]?.also {
resultCallbacks.remove(requestCode) 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
resultCallbacks[requestCode]?.apply { resultCallbacks[requestCode]?.also {
resultCallbacks.remove(requestCode) 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.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import com.topjohnwu.magisk.wrap import com.topjohnwu.magisk.core.wrap
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
abstract class BaseReceiver : BroadcastReceiver(), KoinComponent { abstract class BaseReceiver : BroadcastReceiver(), KoinComponent {
......
package com.topjohnwu.magisk.base package com.topjohnwu.magisk.core.base
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import com.topjohnwu.magisk.wrap import com.topjohnwu.magisk.core.wrap
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
abstract class BaseService : Service(), KoinComponent { abstract class BaseService : Service(), KoinComponent {
......
package com.topjohnwu.magisk.base package com.topjohnwu.magisk.core.base
import android.content.Context import android.content.Context
import android.net.Network import android.net.Network
...@@ -10,7 +10,7 @@ import androidx.work.ListenableWorker ...@@ -10,7 +10,7 @@ import androidx.work.ListenableWorker
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import java.util.* import java.util.*
abstract class DelegateWorker { abstract class BaseWorkerWrapper {
private lateinit var worker: ListenableWorker private lateinit var worker: ListenableWorker
......
package com.topjohnwu.magisk.model.download package com.topjohnwu.magisk.core.download
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Notification import android.app.Notification
...@@ -8,15 +8,15 @@ import android.content.Intent ...@@ -8,15 +8,15 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.extensions.chooser import com.topjohnwu.magisk.extensions.chooser
import com.topjohnwu.magisk.extensions.exists import com.topjohnwu.magisk.extensions.exists
import com.topjohnwu.magisk.extensions.provide 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.*
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary 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.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 com.topjohnwu.magisk.utils.APKInstall
import org.koin.core.get import org.koin.core.get
import java.io.File import java.io.File
...@@ -114,13 +114,15 @@ open class DownloadService : RemoteFileService() { ...@@ -114,13 +114,15 @@ open class DownloadService : RemoteFileService() {
@Suppress("ReplaceSingleLineLet") @Suppress("ReplaceSingleLineLet")
private fun Notification.Builder.setContentIntent(intent: Intent) = private fun Notification.Builder.setContentIntent(intent: Intent) =
setContentIntent(
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT) PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
.let { setContentIntent(it) } )
@Suppress("ReplaceSingleLineLet") @Suppress("ReplaceSingleLineLet")
private fun Notification.Builder.addAction(icon: Int, title: Int, intent: Intent) = private fun Notification.Builder.addAction(icon: Int, title: Int, intent: Intent) =
addAction(icon, getString(title),
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT) PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
.let { addAction(icon, getString(title), it) } )
// --- // ---
......
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.extensions.writeTo
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Restore 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.Configuration.APK.Upgrade
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.utils.PatchAPK
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import java.io.File import java.io.File
......
package com.topjohnwu.magisk.model.download package com.topjohnwu.magisk.core.download
import com.topjohnwu.magisk.extensions.withStreams import com.topjohnwu.magisk.extensions.withStreams
import java.io.File import java.io.File
......
package com.topjohnwu.magisk.model.download package com.topjohnwu.magisk.core.download
import android.app.Notification import android.app.Notification
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import com.topjohnwu.magisk.base.BaseService import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.core.view.Notifications
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import java.util.* import java.util.*
import kotlin.random.Random.Default.nextInt 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.Activity
import android.app.Notification import android.app.Notification
import android.content.Intent import android.content.Intent
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.di.NullActivity import com.topjohnwu.magisk.di.NullActivity
...@@ -11,12 +13,13 @@ import com.topjohnwu.magisk.extensions.subscribeK ...@@ -11,12 +13,13 @@ import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo 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.model.entity.internal.DownloadSubject.* import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
import com.topjohnwu.magisk.utils.ProgressInputStream import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.core.view.Notifications
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
import io.reactivex.Completable import io.reactivex.Completable
import okhttp3.ResponseBody import okhttp3.ResponseBody
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.core.KoinComponent
import timber.log.Timber import timber.log.Timber
import java.io.InputStream import java.io.InputStream
...@@ -39,11 +42,7 @@ abstract class RemoteFileService : NotificationService() { ...@@ -39,11 +42,7 @@ abstract class RemoteFileService : NotificationService() {
.doOnSubscribe { update(subject.hashCode()) { it.setContentTitle(subject.title) } } .doOnSubscribe { update(subject.hashCode()) { it.setContentTitle(subject.title) } }
.subscribeK(onError = { .subscribeK(onError = {
Timber.e(it) Timber.e(it)
finishNotify(subject.hashCode()) { notification -> failNotify(subject)
notification.setContentText(getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
}
}) { }) {
val newId = finishNotify(subject) val newId = finishNotify(subject)
if (get<Activity>() !is NullActivity) { if (get<Activity>() !is NullActivity) {
...@@ -62,7 +61,7 @@ abstract class RemoteFileService : NotificationService() { ...@@ -62,7 +61,7 @@ abstract class RemoteFileService : NotificationService() {
} }
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url) private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
.map { it.toStream(subject.hashCode()) } .map { it.toStream(subject.hashCode(), subject) }
.flatMapCompletable { stream -> .flatMapCompletable { stream ->
when (subject) { when (subject) {
is Module -> service.fetchInstaller() is Module -> service.fetchInstaller()
...@@ -75,7 +74,7 @@ abstract class RemoteFileService : NotificationService() { ...@@ -75,7 +74,7 @@ abstract class RemoteFileService : NotificationService() {
handleAPK(subject) handleAPK(subject)
} }
private fun ResponseBody.toStream(id: Int): InputStream { private fun ResponseBody.toStream(id: Int, subject: DownloadSubject): InputStream {
val maxRaw = contentLength() val maxRaw = contentLength()
val max = maxRaw / 1_000_000f val max = maxRaw / 1_000_000f
...@@ -83,17 +82,27 @@ abstract class RemoteFileService : NotificationService() { ...@@ -83,17 +82,27 @@ abstract class RemoteFileService : NotificationService() {
val progress = it / 1_000_000f val progress = it / 1_000_000f
update(id) { notification -> update(id) { notification ->
if (maxRaw > 0) { if (maxRaw > 0) {
send(progress / max, subject)
notification notification
.setProgress(maxRaw.toInt(), it.toInt(), false) .setProgress(maxRaw.toInt(), it.toInt(), false)
.setContentText("%.2f / %.2f MB".format(progress, max)) .setContentText("%.2f / %.2f MB".format(progress, max))
} else { } else {
send(-1f, subject)
notification.setContentText("%.2f MB / ??".format(progress)) 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()) { private fun finishNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) {
send(1f, subject)
it.addActions(subject) it.addActions(subject)
.setContentText(getString(R.string.download_complete)) .setContentText(getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done) .setSmallIcon(android.R.drawable.stat_sys_download_done)
...@@ -111,8 +120,19 @@ abstract class RemoteFileService : NotificationService() { ...@@ -111,8 +120,19 @@ abstract class RemoteFileService : NotificationService() {
protected abstract fun Notification.Builder.addActions(subject: DownloadSubject) protected abstract fun Notification.Builder.addActions(subject: DownloadSubject)
: Notification.Builder : Notification.Builder
companion object { companion object : KoinComponent {
const val ARG_URL = "arg_url" 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 androidx.annotation.StringDef
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
......
package com.topjohnwu.magisk.data.database package com.topjohnwu.magisk.core.magiskdb
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.data.database.magiskdb.BaseDao import com.topjohnwu.magisk.core.model.MagiskPolicy
import com.topjohnwu.magisk.data.database.magiskdb.Delete import com.topjohnwu.magisk.core.model.toMap
import com.topjohnwu.magisk.data.database.magiskdb.Replace import com.topjohnwu.magisk.core.model.toPolicy
import com.topjohnwu.magisk.data.database.magiskdb.Select
import com.topjohnwu.magisk.extensions.now 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 timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
......
package com.topjohnwu.magisk.data.database.magiskdb package com.topjohnwu.magisk.core.magiskdb
import androidx.annotation.StringDef import androidx.annotation.StringDef
......
package com.topjohnwu.magisk.data.database package com.topjohnwu.magisk.core.magiskdb
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
class SettingsDao : BaseDao() { class SettingsDao : BaseDao() {
......
package com.topjohnwu.magisk.data.database package com.topjohnwu.magisk.core.magiskdb
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
class StringDao : BaseDao() { class StringDao : BaseDao() {
......
package com.topjohnwu.magisk.model.entity package com.topjohnwu.magisk.core.model
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.INTERACTIVE
import com.topjohnwu.magisk.extensions.getLabel import com.topjohnwu.magisk.extensions.getLabel
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE
data class MagiskPolicy( data class MagiskPolicy(
......
package com.topjohnwu.magisk.model.entity package com.topjohnwu.magisk.core.model
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize 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 class BaseModule : Comparable<BaseModule> {
abstract var id: String abstract var id: String
......
package com.topjohnwu.magisk.model.entity.module package com.topjohnwu.magisk.core.model.module
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile 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 android.os.Parcelable
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey 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.data.repository.StringRepository
import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.legalFilename import com.topjohnwu.magisk.extensions.legalFilename
......
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.core.su
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
...@@ -6,20 +6,26 @@ import android.os.Build ...@@ -6,20 +6,26 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Process import android.os.Process
import android.widget.Toast 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.data.repository.LogRepository
import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.startActivity import com.topjohnwu.magisk.extensions.startActivity
import com.topjohnwu.magisk.extensions.startActivityWithRoot import com.topjohnwu.magisk.extensions.startActivityWithRoot
import com.topjohnwu.magisk.extensions.subscribeK 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.toLog
import com.topjohnwu.magisk.model.entity.toPolicy import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import timber.log.Timber import timber.log.Timber
object SuHandler : ProviderCallHandler { object SuCallbackHandler : ProviderCallHandler {
const val REQUEST = "request" const val REQUEST = "request"
const val LOG = "log" const val LOG = "log"
......
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.core.su
import android.net.LocalSocket import android.net.LocalSocket
import android.net.LocalSocketAddress 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.content.Context
import android.net.Uri 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.fileName
import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.readUri import com.topjohnwu.magisk.extensions.readUri
import com.topjohnwu.magisk.extensions.subscribeK 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 com.topjohnwu.superuser.Shell
import io.reactivex.Single import io.reactivex.Single
import java.io.File import java.io.File
......
package com.topjohnwu.magisk.tasks package com.topjohnwu.magisk.core.tasks
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
...@@ -6,11 +6,13 @@ import android.os.Build ...@@ -6,11 +6,13 @@ import android.os.Build
import android.text.TextUtils import android.text.TextUtils
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.topjohnwu.magisk.Config import androidx.core.net.toUri
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.data.network.GithubRawServices
import com.topjohnwu.magisk.di.Protected import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.extensions.* import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.net.Networking
import com.topjohnwu.signing.SignBoot import com.topjohnwu.signing.SignBoot
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
...@@ -34,10 +36,10 @@ import java.util.zip.ZipInputStream ...@@ -34,10 +36,10 @@ import java.util.zip.ZipInputStream
abstract class MagiskInstaller { abstract class MagiskInstaller {
protected lateinit var srcBoot: String
protected lateinit var destFile: File
protected lateinit var installDir: 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 console: MutableList<String>
private val logs: MutableList<String> private val logs: MutableList<String>
...@@ -60,7 +62,7 @@ abstract class MagiskInstaller { ...@@ -60,7 +62,7 @@ abstract class MagiskInstaller {
installDir.mkdirs() installDir.mkdirs()
} }
protected fun findImage(): Boolean { private fun findImage(): Boolean {
srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh() srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
if (srcBoot.isEmpty()) { if (srcBoot.isEmpty()) {
console.add("! Unable to detect target image") console.add("! Unable to detect target image")
...@@ -70,7 +72,7 @@ abstract class MagiskInstaller { ...@@ -70,7 +72,7 @@ abstract class MagiskInstaller {
return true return true
} }
protected fun findSecondaryImage(): Boolean { private fun findSecondaryImage(): Boolean {
val slot = "echo \$SLOT".fsh() val slot = "echo \$SLOT".fsh()
val target = if (slot == "_a") "_b" else "_a" val target = if (slot == "_a") "_b" else "_a"
console.add("- Target slot: $target") console.add("- Target slot: $target")
...@@ -87,7 +89,7 @@ abstract class MagiskInstaller { ...@@ -87,7 +89,7 @@ abstract class MagiskInstaller {
return true return true
} }
protected fun extractZip(): Boolean { private fun extractZip(): Boolean {
val arch: String val arch: String
arch = if (Build.VERSION.SDK_INT >= 21) { arch = if (Build.VERSION.SDK_INT >= 21) {
val abis = listOf(*Build.SUPPORTED_ABIS) val abis = listOf(*Build.SUPPORTED_ABIS)
...@@ -208,7 +210,7 @@ abstract class MagiskInstaller { ...@@ -208,7 +210,7 @@ abstract class MagiskInstaller {
} }
} }
protected fun handleFile(uri: Uri): Boolean { private fun handleFile(uri: Uri): Boolean {
try { try {
context.readUri(uri).buffered().use { context.readUri(uri).buffered().use {
it.mark(500) it.mark(500)
...@@ -238,7 +240,7 @@ abstract class MagiskInstaller { ...@@ -238,7 +240,7 @@ abstract class MagiskInstaller {
return true return true
} }
protected fun patchBoot(): Boolean { private fun patchBoot(): Boolean {
var isSigned = false var isSigned = false
try { try {
SuFileInputStream(srcBoot).use { SuFileInputStream(srcBoot).use {
...@@ -284,7 +286,7 @@ abstract class MagiskInstaller { ...@@ -284,7 +286,7 @@ abstract class MagiskInstaller {
return true return true
} }
protected fun flashBoot(): Boolean { private fun flashBoot(): Boolean {
if (!"direct_install $installDir $srcBoot".sh().isSuccess) if (!"direct_install $installDir $srcBoot".sh().isSuccess)
return false return false
arrayOf( arrayOf(
...@@ -294,7 +296,7 @@ abstract class MagiskInstaller { ...@@ -294,7 +296,7 @@ abstract class MagiskInstaller {
return true return true
} }
protected fun storeBoot(): Boolean { private fun storeBoot(): Boolean {
val patched = SuFile.open(installDir, "new-boot.img") val patched = SuFile.open(installDir, "new-boot.img")
try { try {
val os = tarOut?.let { val os = tarOut?.let {
...@@ -320,7 +322,7 @@ abstract class MagiskInstaller { ...@@ -320,7 +322,7 @@ abstract class MagiskInstaller {
return true return true
} }
protected fun postOTA(): Boolean { private fun postOTA(): Boolean {
val bootctl = SuFile("/data/adb/bootctl") val bootctl = SuFile("/data/adb/bootctl")
try { try {
withStreams(service.fetchBootctl().blockingGet().byteStream(), bootctl.suOutputStream()) { withStreams(service.fetchBootctl().blockingGet().byteStream(), bootctl.suOutputStream()) {
...@@ -345,6 +347,28 @@ abstract class MagiskInstaller { ...@@ -345,6 +347,28 @@ abstract class MagiskInstaller {
private fun String.fsh() = ShellUtils.fastCmd(this) private fun String.fsh() = ShellUtils.fastCmd(this)
private fun Array<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 @WorkerThread
protected abstract fun operations(): Boolean 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.database.RepoDao
import com.topjohnwu.magisk.data.network.GithubApiServices 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.Flowable
import io.reactivex.Single
import io.reactivex.rxkotlin.toFlowable import io.reactivex.rxkotlin.toFlowable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import se.ansman.kotshi.JsonSerializable import se.ansman.kotshi.JsonSerializable
...@@ -19,17 +19,16 @@ class RepoUpdater( ...@@ -19,17 +19,16 @@ class RepoUpdater(
private val api: GithubApiServices, private val api: GithubApiServices,
private val repoDB: RepoDao private val repoDB: RepoDao
) { ) {
private fun loadRepos(repos: List<GithubRepoInfo>, cached: MutableSet<String>) = private fun loadRepos(repos: List<GithubRepoInfo>, cached: MutableSet<String>) =
repos.toFlowable().parallel().runOn(Schedulers.io()).map { repos.toFlowable().parallel().runOn(Schedulers.io()).map {
// Skip submission // Skip submission
if (it.id == "submission") if (it.id == "submission")
return@map return@map
(repoDB.getRepo(it.id)?.apply { cached.remove(it.id) } ?: val repo = repoDB.getRepo(it.id)?.apply { cached.remove(it.id) } ?: Repo(it.id)
Repo(it.id)).runCatching { repo.runCatching {
update(it.pushDate) update(it.pushDate)
repoDB.addRepo(this) repoDB.addRepo(this)
}.getOrElse { Timber.e(it) } }.getOrElse(Timber::e)
}.sequential() }.sequential()
private fun loadPage( private fun loadPage(
...@@ -40,7 +39,7 @@ class RepoUpdater( ...@@ -40,7 +39,7 @@ class RepoUpdater(
it.error()?.also { throw it } it.error()?.also { throw it }
it.response()?.run { it.response()?.run {
if (code() == HttpURLConnection.HTTP_NOT_MODIFIED) if (code() == HttpURLConnection.HTTP_NOT_MODIFIED)
return@run Flowable.error<Unit>(CachedException) return@run Flowable.error<Unit>(CachedException())
if (page == 1) if (page == 1)
repoDB.etagKey = headers()[Const.Key.ETAG_KEY].orEmpty().trimEtag() repoDB.etagKey = headers()[Const.Key.ETAG_KEY].orEmpty().trimEtag()
...@@ -58,14 +57,17 @@ class RepoUpdater( ...@@ -58,14 +57,17 @@ class RepoUpdater(
cached.toFlowable().parallel().runOn(Schedulers.io()).map { cached.toFlowable().parallel().runOn(Schedulers.io()).map {
runCatching { runCatching {
Repo(it).update() Repo(it).update()
}.getOrElse { Timber.e(it) } }.getOrElse(Timber::e)
}.sequential() }.sequential()
private fun String.trimEtag() = substring(indexOf('\"'), lastIndexOf('\"') + 1) private fun String.trimEtag() = substring(indexOf('\"'), lastIndexOf('\"') + 1)
operator fun invoke(forced: Boolean = false) : Single<Unit> { @Suppress("RedundantLambdaArrow")
val cached = Collections.synchronizedSet(HashSet(repoDB.repoIDList)) operator fun invoke(forced: Boolean) : Completable {
return loadPage(cached, etag = repoDB.etagKey).doOnComplete { return Flowable
.fromCallable { Collections.synchronizedSet(HashSet(repoDB.repoIDList)) }
.flatMap { cached ->
loadPage(cached, etag = repoDB.etagKey).doOnComplete {
repoDB.removeRepos(cached) repoDB.removeRepos(cached)
}.onErrorResumeNext { it: Throwable -> }.onErrorResumeNext { it: Throwable ->
if (it is CachedException) { if (it is CachedException) {
...@@ -75,10 +77,11 @@ class RepoUpdater( ...@@ -75,10 +77,11 @@ class RepoUpdater(
Timber.e(it) Timber.e(it)
} }
Flowable.empty() Flowable.empty()
}.ignoreElements().toSingleDefault(Unit) }
}.ignoreElements()
} }
object CachedException : Exception() class CachedException : Exception()
} }
private val dateFormat: SimpleDateFormat = private val dateFormat: SimpleDateFormat =
......
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.core.utils
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import org.koin.core.get import org.koin.core.get
......
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.core.utils
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.util.Base64 import android.util.Base64
import android.util.Base64OutputStream 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.di.koinModules
import com.topjohnwu.magisk.utils.PatchAPK.ALPHANUM
import com.topjohnwu.signing.CryptoUtils.readCertificate import com.topjohnwu.signing.CryptoUtils.readCertificate
import com.topjohnwu.signing.CryptoUtils.readPrivateKey import com.topjohnwu.signing.CryptoUtils.readPrivateKey
import com.topjohnwu.superuser.internal.InternalUtils import com.topjohnwu.superuser.internal.InternalUtils
...@@ -50,10 +50,14 @@ class Keygen: CertKeyProvider { ...@@ -50,10 +50,14 @@ class Keygen: CertKeyProvider {
private val provider: CertKeyProvider private val provider: CertKeyProvider
inner class KeyStoreProvider : CertKeyProvider { inner class KeyStoreProvider :
CertKeyProvider {
private val ks by lazy { init() } private val ks by lazy { init() }
override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate } 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 { class TestProvider : CertKeyProvider {
...@@ -113,8 +117,12 @@ class Keygen: CertKeyProvider { ...@@ -113,8 +117,12 @@ class Keygen: CertKeyProvider {
if (raw.isEmpty()) { if (raw.isEmpty()) {
ks.load(null) ks.load(null)
} else { } else {
GZIPInputStream(Base64.decode(raw, BASE64_FLAG).inputStream()).use { GZIPInputStream(Base64.decode(raw,
ks.load(it, PASSWORD) BASE64_FLAG
).inputStream()).use {
ks.load(it,
PASSWORD
)
} }
} }
...@@ -131,10 +139,16 @@ class Keygen: CertKeyProvider { ...@@ -131,10 +139,16 @@ class Keygen: CertKeyProvider {
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer)) val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
// Store them into keystore // Store them into keystore
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert)) ks.setKeyEntry(
ALIAS, kp.private,
PASSWORD, arrayOf(cert))
val bytes = ByteArrayOutputStream() val bytes = ByteArrayOutputStream()
GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use { GZIPOutputStream(Base64OutputStream(bytes,
ks.store(it, PASSWORD) BASE64_FLAG
)).use {
ks.store(it,
PASSWORD
)
} }
Config.keyStoreRaw = bytes.toString("UTF-8") Config.keyStoreRaw = bytes.toString("UTF-8")
......
@file:Suppress("DEPRECATION") @file:Suppress("DEPRECATION")
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.core.utils
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R 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.langTagToLocale
import com.topjohnwu.magisk.extensions.toLangTag import com.topjohnwu.magisk.extensions.toLangTag
import io.reactivex.Single import io.reactivex.Single
......
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.core.utils
import android.content.Context import android.content.Context
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.widget.Toast 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.data.network.GithubRawServices
import com.topjohnwu.magisk.extensions.DynamicClassLoader import com.topjohnwu.magisk.extensions.DynamicClassLoader
import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo 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.JarMap
import com.topjohnwu.signing.SignAPK import com.topjohnwu.signing.SignAPK
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
......
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.core.utils
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
import java.io.FilterInputStream import java.io.FilterInputStream
......
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.core.utils
import android.content.Context import android.content.Context
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R 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.extensions.rawResource
import com.topjohnwu.magisk.wrap
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile import com.topjohnwu.superuser.io.SuFile
......
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.core.utils
interface SafetyNetHelper { interface SafetyNetHelper {
...@@ -12,10 +12,10 @@ interface SafetyNetHelper { ...@@ -12,10 +12,10 @@ interface SafetyNetHelper {
companion object { companion object {
val RESPONSE_ERR = 0x01 const val RESPONSE_ERR = 0x01
val CONNECTION_FAIL = 0x02 const val CONNECTION_FAIL = 0x02
val BASIC_PASS = 0x10 const val BASIC_PASS = 0x10
val CTS_PASS = 0x20 const val CTS_PASS = 0x20
} }
} }
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.core.utils
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
...@@ -7,10 +7,10 @@ import android.net.Uri ...@@ -7,10 +7,10 @@ import android.net.Uri
import android.os.Environment import android.os.Environment
import android.widget.Toast import android.widget.Toast
import androidx.work.* import androidx.work.*
import com.topjohnwu.magisk.* import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.model.update.UpdateCheckService
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
...@@ -62,7 +62,10 @@ object Utils { ...@@ -62,7 +62,10 @@ object Utils {
if (intent.resolveActivity(context.packageManager) != null) { if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent) context.startActivity(intent)
} else { } 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.SuFile
import com.topjohnwu.superuser.io.SuFileOutputStream import com.topjohnwu.superuser.io.SuFileOutputStream
......
package com.topjohnwu.magisk.view package com.topjohnwu.magisk.core.view
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
...@@ -9,13 +9,12 @@ import android.os.Build.VERSION.SDK_INT ...@@ -9,13 +9,12 @@ import android.os.Build.VERSION.SDK_INT
import androidx.core.app.TaskStackBuilder import androidx.core.app.TaskStackBuilder
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toIcon import androidx.core.graphics.drawable.toIcon
import com.topjohnwu.magisk.* import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.Const.ID.PROGRESS_NOTIFICATION_CHANNEL import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.Const.ID.UPDATE_NOTIFICATION_CHANNEL 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.get
import com.topjohnwu.magisk.extensions.getBitmap import com.topjohnwu.magisk.extensions.getBitmap
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.ui.SplashActivity
object Notifications { object Notifications {
...@@ -52,10 +51,13 @@ object Notifications { ...@@ -52,10 +51,13 @@ object Notifications {
val stackBuilder = TaskStackBuilder.create(context) val stackBuilder = TaskStackBuilder.create(context)
stackBuilder.addParentStack(SplashActivity::class.java.cmp(context.packageName)) stackBuilder.addParentStack(SplashActivity::class.java.cmp(context.packageName))
stackBuilder.addNextIntent(intent) 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) PendingIntent.FLAG_UPDATE_CURRENT)
val builder = updateBuilder(context) val builder = updateBuilder(
context
)
.setContentTitle(context.getString(R.string.magisk_update_title)) .setContentTitle(context.getString(R.string.magisk_update_title))
.setContentText(context.getString(R.string.manager_download_install)) .setContentText(context.getString(R.string.manager_download_install))
.setAutoCancel(true) .setAutoCancel(true)
...@@ -72,7 +74,9 @@ object Notifications { ...@@ -72,7 +74,9 @@ object Notifications {
val pendingIntent = PendingIntent.getBroadcast(context, val pendingIntent = PendingIntent.getBroadcast(context,
Const.ID.APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT) 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)) .setContentTitle(context.getString(R.string.manager_update_title))
.setContentText(context.getString(R.string.manager_download_install)) .setContentText(context.getString(R.string.manager_download_install))
.setAutoCancel(true) .setAutoCancel(true)
...@@ -87,7 +91,9 @@ object Notifications { ...@@ -87,7 +91,9 @@ object Notifications {
val pendingIntent = PendingIntent.getBroadcast(context, val pendingIntent = PendingIntent.getBroadcast(context,
Const.ID.DTBO_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT) 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)) .setContentTitle(context.getString(R.string.dtbo_patched_title))
.setContentText(context.getString(R.string.dtbo_patched_reboot)) .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.Context
import android.content.Intent import android.content.Intent
...@@ -10,17 +10,18 @@ import androidx.annotation.RequiresApi ...@@ -10,17 +10,18 @@ import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toAdaptiveIcon import androidx.core.graphics.drawable.toAdaptiveIcon
import androidx.core.graphics.drawable.toIcon 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.extensions.getBitmap
import com.topjohnwu.magisk.ui.SplashActivity
import com.topjohnwu.magisk.utils.Utils
object Shortcuts { object Shortcuts {
fun setup(context: Context) { fun setup(context: Context) {
if (Build.VERSION.SDK_INT >= 25) { if (Build.VERSION.SDK_INT >= 25) {
val manager = context.getSystemService<ShortcutManager>() val manager = context.getSystemService<ShortcutManager>()
manager?.dynamicShortcuts = getShortCuts(context) manager?.dynamicShortcuts =
getShortCuts(context)
} }
} }
...@@ -77,19 +78,6 @@ object Shortcuts { ...@@ -77,19 +78,6 @@ object Shortcuts {
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
) )
.setIcon(getIcon(R.drawable.sc_extension)) .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) .setRank(2)
.build() .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 package com.topjohnwu.magisk.data.database
import androidx.room.* import androidx.room.*
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.model.entity.module.Repo import com.topjohnwu.magisk.core.model.module.Repo
@Database(version = 6, entities = [Repo::class, RepoEtag::class]) @Database(version = 6, entities = [Repo::class, RepoEtag::class])
abstract class RepoDatabase : RoomDatabase() { abstract class RepoDatabase : RoomDatabase() {
abstract fun repoDao() : RepoDao abstract fun repoDao() : RepoDao
abstract fun repoByUpdatedDao(): RepoByUpdatedDao
abstract fun repoByNameDao(): RepoByNameDao
} }
@Dao @Dao
......
package com.topjohnwu.magisk.data.network package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.model.entity.UpdateInfo import com.topjohnwu.magisk.core.tasks.GithubRepoInfo
import com.topjohnwu.magisk.tasks.GithubRepoInfo import com.topjohnwu.magisk.core.model.UpdateInfo
import io.reactivex.Flowable import io.reactivex.Flowable
import io.reactivex.Single import io.reactivex.Single
import okhttp3.ResponseBody import okhttp3.ResponseBody
......
package com.topjohnwu.magisk.data.repository package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.data.database.SettingsDao import com.topjohnwu.magisk.core.magiskdb.SettingsDao
import com.topjohnwu.magisk.data.database.StringDao import com.topjohnwu.magisk.core.magiskdb.StringDao
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlin.properties.ReadWriteProperty import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
......
package com.topjohnwu.magisk.data.repository 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.data.database.SuLogDao
import com.topjohnwu.magisk.model.entity.MagiskLog import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Single import io.reactivex.Single
import java.util.concurrent.TimeUnit
class LogRepository( class LogRepository(
private val logDao: SuLogDao private val logDao: SuLogDao
) { ) {
fun fetchLogs() = logDao.fetchAll().map { it.wrap() } fun fetchLogs() = logDao.fetchAll()
fun fetchMagiskLogs() = Single.fromCallable { fun fetchMagiskLogs() = Single.fromCallable {
Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").exec().out Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").exec().out
...@@ -28,11 +26,4 @@ class LogRepository( ...@@ -28,11 +26,4 @@ class LogRepository(
fun insert(log: MagiskLog) = logDao.insert(log) 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 package com.topjohnwu.magisk.data.repository
import android.content.pm.PackageManager import android.content.pm.PackageManager
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.Info import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.extensions.getLabel import com.topjohnwu.magisk.extensions.getLabel
import com.topjohnwu.magisk.extensions.packageName import com.topjohnwu.magisk.extensions.packageName
...@@ -24,7 +24,8 @@ class MagiskRepository( ...@@ -24,7 +24,8 @@ class MagiskRepository(
Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate() Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate() Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate()
Config.Value.CANARY_DEBUG_CHANNEL -> apiRaw.fetchCanaryDebugUpdate() 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() else -> throw IllegalArgumentException()
}.flatMap { }.flatMap {
// If remote version is lower than current installed, try switching to beta // If remote version is lower than current installed, try switching to beta
......
package com.topjohnwu.magisk.data.repository 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.data.network.GithubRawServices
import com.topjohnwu.magisk.model.entity.module.Repo
class StringRepository( class StringRepository(
private val api: GithubRawServices private val api: GithubRawServices
......
...@@ -6,6 +6,7 @@ import android.app.Application ...@@ -6,6 +6,7 @@ import android.app.Application
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.topjohnwu.magisk.utils.RxBus import com.topjohnwu.magisk.utils.RxBus
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
...@@ -23,6 +24,7 @@ val applicationModule = module { ...@@ -23,6 +24,7 @@ val applicationModule = module {
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) } single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
single { ActivityTracker() } single { ActivityTracker() }
factory { get<ActivityTracker>().foreground ?: NullActivity } factory { get<ActivityTracker>().foreground ?: NullActivity }
single { LocalBroadcastManager.getInstance(get()) }
} }
private fun createDEContext(context: Context): Context { private fun createDEContext(context: Context): Context {
......
...@@ -2,8 +2,12 @@ package com.topjohnwu.magisk.di ...@@ -2,8 +2,12 @@ package com.topjohnwu.magisk.di
import android.content.Context import android.content.Context
import androidx.room.Room import androidx.room.Room
import com.topjohnwu.magisk.data.database.* import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.tasks.RepoUpdater 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 import org.koin.dsl.module
...@@ -11,7 +15,10 @@ val databaseModule = module { ...@@ -11,7 +15,10 @@ val databaseModule = module {
single { PolicyDao(get()) } single { PolicyDao(get()) }
single { SettingsDao() } single { SettingsDao() }
single { StringDao() } 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 { createSuLogDatabase(get(Protected)).suLogDao() }
single { RepoUpdater(get(), get()) } single { RepoUpdater(get(), get()) }
} }
......
...@@ -4,7 +4,7 @@ import android.content.Context ...@@ -4,7 +4,7 @@ import android.content.Context
import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.topjohnwu.magisk.BuildConfig 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.GithubApiServices
import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.net.Networking import com.topjohnwu.magisk.net.Networking
......
package com.topjohnwu.magisk.di package com.topjohnwu.magisk.di
import android.net.Uri 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.MainViewModel
import com.topjohnwu.magisk.ui.flash.FlashViewModel
import com.topjohnwu.magisk.ui.hide.HideViewModel import com.topjohnwu.magisk.ui.hide.HideViewModel
import com.topjohnwu.magisk.ui.home.HomeViewModel 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.log.LogViewModel
import com.topjohnwu.magisk.ui.module.ModuleViewModel 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.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.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val viewModelModules = module { val viewModelModules = module {
viewModel { MainViewModel() } viewModel { HideViewModel(get()) }
viewModel { HomeViewModel(get()) } viewModel { HomeViewModel(get()) }
viewModel { SuperuserViewModel(get(), get(), get(), get()) } viewModel { LogViewModel(get()) }
viewModel { HideViewModel(get(), get()) }
viewModel { ModuleViewModel(get(), get(), 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) -> viewModel { (action: String, file: Uri, additional: Uri) ->
FlashViewModel(action, file, additional, get()) FlashViewModel(action, file, additional, get())
} }
......
...@@ -52,9 +52,9 @@ fun <T> Observable<T>.subscribeK( ...@@ -52,9 +52,9 @@ fun <T> Observable<T>.subscribeK(
fun <T> Single<T>.subscribeK( fun <T> Single<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() }, onError: OnErrorListener = { it.printStackTrace() },
onSuccess: OnSuccessListener<T> = {} onNext: OnSuccessListener<T> = {}
) = applySchedulers() ) = applySchedulers()
.subscribe(onSuccess, onError) .subscribe(onNext, onError)
fun <T> Maybe<T>.subscribeK( fun <T> Maybe<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() }, onError: OnErrorListener = { it.printStackTrace() },
...@@ -198,5 +198,8 @@ fun <T> ObservableField<T>.toObservable(): Observable<T> { ...@@ -198,5 +198,8 @@ fun <T> ObservableField<T>.toObservable(): Observable<T> {
fun <T : Any> T.toSingle() = Single.just(this) fun <T : Any> T.toSingle() = Single.just(this)
fun <T1, T2, R> zip(t1: Single<T1>, t2: Single<T2>, zipper: (T1, T2) -> R) = inline fun <T1, T2, R> zip(
Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) }) 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 ...@@ -28,14 +28,17 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.FileProvider 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.DynamicClassLoader
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.magisk.utils.currentLocale
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.text.SimpleDateFormat
import java.util.*
import java.lang.reflect.Array as JArray import java.lang.reflect.Array as JArray
val packageName: String get() = get<Context>().packageName val packageName: String get() = get<Context>().packageName
...@@ -283,7 +286,7 @@ fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.getDrawable(thi ...@@ -283,7 +286,7 @@ fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.getDrawable(thi
* with respect to RTL layout direction * with respect to RTL layout direction
*/ */
fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> { 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 resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
) { ) {
return end to start return end to start
...@@ -294,10 +297,10 @@ fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> { ...@@ -294,10 +297,10 @@ fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
fun Context.openUrl(url: String) = Utils.openLink(this, url.toUri()) fun Context.openUrl(url: String) = Utils.openLink(this, url.toUri())
@Suppress("FunctionName") @Suppress("FunctionName")
inline fun <reified T> T.DynamicClassLoader(apk: File) inline fun <reified T> T.DynamicClassLoader(apk: File) =
= DynamicClassLoader(apk, T::class.java.classLoader) DynamicClassLoader(apk, T::class.java.classLoader)
fun Context.unwrap() : Context { fun Context.unwrap(): Context {
var context = this var context = this
while (true) { while (true) {
if (context is ContextWrapper) if (context is ContextWrapper)
...@@ -309,3 +312,38 @@ fun Context.unwrap() : Context { ...@@ -309,3 +312,38 @@ fun Context.unwrap() : Context {
} }
fun Uri.writeTo(file: File) = toFile().copyTo(file) 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 package com.topjohnwu.magisk.extensions
import android.os.Build import android.os.Build
import timber.log.Timber
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.lang.reflect.Field import java.lang.reflect.Field
import java.lang.reflect.Method import java.lang.reflect.Method
import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
...@@ -100,6 +102,9 @@ fun Locale.toLangTag(): String { ...@@ -100,6 +102,9 @@ fun Locale.toLangTag(): String {
} }
} }
fun SimpleDateFormat.parseOrNull(date: String) =
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
// Reflection hacks // Reflection hacks
private val loadClass = ClassLoader::class.java.getMethod("loadClass", String::class.java) private val loadClass = ClassLoader::class.java.getMethod("loadClass", String::class.java)
......
package com.topjohnwu.magisk.extensions 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.Shell
import com.topjohnwu.superuser.io.SuFileInputStream import com.topjohnwu.superuser.io.SuFileInputStream
import com.topjohnwu.superuser.io.SuFileOutputStream import com.topjohnwu.superuser.io.SuFileOutputStream
...@@ -12,3 +12,5 @@ fun reboot(reason: String = if (Info.recovery) "recovery" else "") { ...@@ -12,3 +12,5 @@ fun reboot(reason: String = if (Info.recovery) "recovery" else "") {
fun File.suOutputStream() = SuFileOutputStream(this) fun File.suOutputStream() = SuFileOutputStream(this)
fun File.suInputStream() = SuFileInputStream(this) fun File.suInputStream() = SuFileInputStream(this)
val hasRoot get() = Shell.rootAccess()
...@@ -2,7 +2,15 @@ package com.topjohnwu.magisk.extensions ...@@ -2,7 +2,15 @@ package com.topjohnwu.magisk.extensions
import android.content.res.Resources 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 { fun String.replaceRandomWithSpecial(): String {
var random: Char var random: Char
......
package com.topjohnwu.magisk.extensions 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.DateFormat
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
...@@ -14,7 +14,22 @@ fun String.toTime(format: DateFormat) = try { ...@@ -14,7 +14,22 @@ fun String.toTime(format: DateFormat) = try {
-1L -1L
} }
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", currentLocale) } val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss",
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", currentLocale) } currentLocale
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, currentLocale) } ) }
val timeFormatTime by lazy { SimpleDateFormat("h:mm a", 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 package com.topjohnwu.magisk.extensions
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver 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) fun View.setOnViewReadyListener(callback: () -> Unit) = addOnGlobalLayoutListener(true, callback)
...@@ -12,3 +16,8 @@ fun View.addOnGlobalLayoutListener(oneShot: Boolean = false, callback: () -> Uni ...@@ -12,3 +16,8 @@ fun View.addOnGlobalLayoutListener(oneShot: Boolean = false, callback: () -> Uni
callback() callback()
} }
}) })
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.Context
import android.content.Intent import android.content.Intent
...@@ -6,22 +6,22 @@ import android.content.pm.ActivityInfo ...@@ -6,22 +6,22 @@ import android.content.pm.ActivityInfo
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.core.net.toUri import androidx.core.net.toUri
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R 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.databinding.ActivityFlashBinding
import com.topjohnwu.magisk.extensions.snackbar import com.topjohnwu.magisk.extensions.snackbar
import com.topjohnwu.magisk.intent
import com.topjohnwu.magisk.model.events.BackPressEvent import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.SnackbarEvent import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.model.events.ViewEvent 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.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import java.io.File 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 layoutRes: Int = R.layout.activity_flash
override val themeRes: Int = R.style.MagiskTheme_Flashing 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.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
...@@ -6,16 +6,16 @@ import android.os.Build ...@@ -6,16 +6,16 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Window import android.view.Window
import com.topjohnwu.magisk.R 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.databinding.ActivityRequestBinding
import com.topjohnwu.magisk.model.events.DieEvent import com.topjohnwu.magisk.model.events.DieEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.model.events.ViewEvent import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.utils.SuHandler import com.topjohnwu.magisk.ui.base.BaseUIActivity
import com.topjohnwu.magisk.utils.SuHandler.REQUEST
import org.koin.androidx.viewmodel.ext.android.viewModel 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 layoutRes: Int = R.layout.activity_request
override val themeRes: Int = R.style.MagiskTheme_SU override val themeRes: Int = R.style.MagiskTheme_SU
...@@ -36,7 +36,11 @@ open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestB ...@@ -36,7 +36,11 @@ open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestB
} }
fun runHandler(action: String?) { fun runHandler(action: String?) {
SuHandler(this, action, intent.extras) SuCallbackHandler(
this,
action,
intent.extras
)
finish() 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( ...@@ -14,3 +14,14 @@ class HideAppInfo(
val processes = info.packageInfo?.processes?.distinct() ?: listOf(info.packageName) 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 ...@@ -3,10 +3,11 @@ package com.topjohnwu.magisk.model.entity
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.PrimaryKey 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.now
import com.topjohnwu.magisk.extensions.timeFormatTime import com.topjohnwu.magisk.extensions.timeFormatTime
import com.topjohnwu.magisk.extensions.toTime import com.topjohnwu.magisk.extensions.toTime
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.ALLOW
@Entity(tableName = "logs") @Entity(tableName = "logs")
data class MagiskLog( data class MagiskLog(
...@@ -23,11 +24,6 @@ data class MagiskLog( ...@@ -23,11 +24,6 @@ data class MagiskLog(
@Ignore val timeString = time.toTime(timeFormatTime) @Ignore val timeString = time.toTime(timeFormatTime)
} }
data class WrappedMagiskLog(
val time: Long,
val items: List<MagiskLog>
)
fun MagiskPolicy.toLog( fun MagiskPolicy.toLog(
toUid: Int, toUid: Int,
fromPid: Int, fromPid: Int,
......
...@@ -2,13 +2,13 @@ package com.topjohnwu.magisk.model.entity.internal ...@@ -2,13 +2,13 @@ package com.topjohnwu.magisk.model.entity.internal
import android.content.Context import android.content.Context
import android.os.Parcelable import android.os.Parcelable
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.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.cachedFile
import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.model.entity.MagiskJson import com.topjohnwu.magisk.core.model.MagiskJson
import com.topjohnwu.magisk.model.entity.ManagerJson import com.topjohnwu.magisk.core.model.ManagerJson
import com.topjohnwu.magisk.model.entity.module.Repo
import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import java.io.File import java.io.File
...@@ -59,7 +59,7 @@ sealed class DownloadSubject : Parcelable { ...@@ -59,7 +59,7 @@ sealed class DownloadSubject : Parcelable {
val magisk: MagiskJson = Info.remote.magisk val magisk: MagiskJson = Info.remote.magisk
@Parcelize @Parcelize
protected data class Flash( data class Flash(
override val configuration: Configuration override val configuration: Configuration
) : Magisk() { ) : Magisk() {
override val url: String get() = magisk.link override val url: String get() = magisk.link
...@@ -72,7 +72,7 @@ sealed class DownloadSubject : Parcelable { ...@@ -72,7 +72,7 @@ sealed class DownloadSubject : Parcelable {
} }
@Parcelize @Parcelize
protected class Uninstall : Magisk() { class Uninstall : Magisk() {
override val configuration: Configuration get() = Configuration.Uninstall override val configuration: Configuration get() = Configuration.Uninstall
override val url: String get() = Info.remote.uninstaller.link override val url: String get() = Info.remote.uninstaller.link
...@@ -83,7 +83,7 @@ sealed class DownloadSubject : Parcelable { ...@@ -83,7 +83,7 @@ sealed class DownloadSubject : Parcelable {
} }
@Parcelize @Parcelize
protected class Download : Magisk() { class Download : Magisk() {
override val configuration: Configuration get() = Configuration.Download override val configuration: Configuration get() = Configuration.Download
override val url: String get() = magisk.link override val url: String get() = magisk.link
......
...@@ -6,7 +6,7 @@ import androidx.databinding.ViewDataBinding ...@@ -6,7 +6,7 @@ import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R 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 val layoutRes: Int = R.layout.item_console
override fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) { override fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {
...@@ -24,3 +24,7 @@ class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() { ...@@ -24,3 +24,7 @@ class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() {
override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other) override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other)
override fun itemSameAs(other: ConsoleRvItem) = item == other.item 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 package com.topjohnwu.magisk.model.entity.recycler
import android.view.View
import android.view.ViewGroup
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback 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.extensions.toggle
import com.topjohnwu.magisk.model.entity.HideAppInfo import com.topjohnwu.magisk.model.entity.ProcessHideApp
import com.topjohnwu.magisk.model.entity.HideTarget import com.topjohnwu.magisk.model.entity.StatefulProcess
import com.topjohnwu.magisk.model.entity.state.IndeterminateState import com.topjohnwu.magisk.model.observer.Observer
import com.topjohnwu.magisk.model.events.HideProcessEvent import com.topjohnwu.magisk.ui.hide.HideViewModel
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus import kotlin.math.roundToInt
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) : class HideItem(val item: ProcessHideApp) : ComparableRvItem<HideItem>() {
ComparableRvItem<HideRvItem>() {
override val layoutRes: Int = R.layout.item_hide_app override val layoutRes = R.layout.item_hide_md2
val packageName = item.info.packageName.orEmpty() val packageName = item.info.info.packageName.orEmpty()
val items = DiffObservableList(callback).also { val items = item.processes.map { HideProcessItem(it) }
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)
private val itemsProcess get() = items.filterIsInstance<HideProcessRvItem>() val isExpanded = KObservableField(false)
val itemsChecked = KObservableField(0)
private val currentState val itemsCheckedPercent = Observer(itemsChecked) {
get() = when (itemsProcess.count { it.isHidden.value }) { (itemsChecked.value.toFloat() / items.size * 100).roundToInt()
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 { init {
itemsProcess.forEach { items.forEach { it.isHidden.addOnPropertyChangedCallback { recalculateChecked() } }
it.isHidden.addOnPropertyChangedCallback { isHiddenState.value = currentState } recalculateChecked()
} }
fun collapse(v: View) {
(v.parent.parent as? ViewGroup)?.startAnimations()
isExpanded.value = false
} }
fun toggle() { fun toggle(v: View) {
val desiredState = when (isHiddenState.value) { (v.parent as? ViewGroup)?.startAnimations()
IndeterminateState.INDETERMINATE, isExpanded.toggle()
IndeterminateState.UNCHECKED -> true
IndeterminateState.CHECKED -> false
} }
itemsProcess.forEach { it.isHidden.value = desiredState }
isHiddenState.value = currentState 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
} }
fun toggleExpansion() { private fun recalculateChecked() {
if (items.size <= 1) return itemsChecked.value = items.count { it.isHidden.value }
isExpanded.toggle()
} }
override fun contentSameAs(other: HideRvItem): Boolean = items.all { other.items.contains(it) } override fun contentSameAs(other: HideItem): Boolean = item == other.item
override fun itemSameAs(other: HideRvItem): Boolean = item.info == other.item.info override fun itemSameAs(other: HideItem): Boolean = item.info == other.item.info
} }
class HideProcessRvItem( class HideProcessItem(val item: StatefulProcess) : ComparableRvItem<HideProcessItem>() {
val packageName: String,
val process: String,
isHidden: Boolean
) : ComparableRvItem<HideProcessRvItem>() {
override val layoutRes: Int = R.layout.item_hide_process override val layoutRes = R.layout.item_hide_process_md2
val isHidden = KObservableField(isHidden) val isHidden = KObservableField(item.isHidden)
private val rxBus: RxBus by inject() fun toggle(viewModel: HideViewModel) {
isHidden.toggle()
init { viewModel.toggleItem(this)
this.isHidden.addOnPropertyChangedCallback {
rxBus.post(HideProcessEvent(this@HideProcessRvItem))
}
} }
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
} }
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 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.R
import com.topjohnwu.magisk.databinding.ComparableRvItem import com.topjohnwu.magisk.extensions.timeDateFormat
import com.topjohnwu.magisk.extensions.timeFormatMedium
import com.topjohnwu.magisk.extensions.toTime 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.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
class LogRvItem : ComparableRvItem<LogRvItem>() { class LogItem(val item: MagiskLog) : ObservableItem<LogItem>() {
override val layoutRes: Int = R.layout.item_page_log
val items = DiffObservableList(callback) override val layoutRes = R.layout.item_log_access_md2
fun update(list: List<LogItemRvItem>) { val date = item.time.toTime(timeDateFormat)
list.firstOrNull()?.isExpanded?.value = true var isTop = false
items.update(list) @Bindable get
set(value) {
field = value
notifyChange(BR.top)
} }
var isBottom = false
//two of these will never be present, safe to assume it's unique @Bindable get
override fun contentSameAs(other: LogRvItem): Boolean = false set(value) {
field = value
override fun itemSameAs(other: LogRvItem): Boolean = false notifyChange(BR.bottom)
}
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 itemSameAs(other: LogItem) = item.appName == other.item.appName
override fun contentSameAs(other: MagiskLogRvItem): Boolean = false
override fun contentSameAs(other: LogItem) = item.fromUid == other.item.fromUid &&
override fun itemSameAs(other: MagiskLogRvItem): Boolean = false 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 package com.topjohnwu.magisk.model.entity.recycler
import android.content.res.Resources import androidx.databinding.Bindable
import androidx.annotation.StringRes 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.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.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback import com.topjohnwu.magisk.ui.module.ModuleViewModel
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.utils.KObservableField import com.topjohnwu.magisk.utils.KObservableField
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() { object SafeModeNotice : ComparableRvItem<SafeModeNotice>() {
override val layoutRes = R.layout.item_safe_mode_notice
override val layoutRes: Int = R.layout.item_module 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
}
val lastActionNotice = KObservableField("") object InstallModule : ComparableRvItem<InstallModule>() {
val isChecked = KObservableField(item.enable) override val layoutRes = R.layout.item_module_download
val isDeletable = KObservableField(item.remove)
init { override fun onBindingBound(binding: ViewDataBinding) {
isChecked.addOnPropertyChangedCallback { super.onBindingBound(binding)
when (it) { val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams
true -> { params?.isFullSpan = true
item.enable = true
notice(R.string.disable_file_removed)
} }
false -> {
item.enable = false override fun contentSameAs(other: InstallModule) = this == other
notice(R.string.disable_file_created) 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) { override fun onBindingBound(binding: ViewDataBinding) {
true -> { super.onBindingBound(binding)
item.remove = true val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams
notice(R.string.remove_file_created) params?.isFullSpan = true
} }
false -> {
item.remove = false override fun itemSameAs(other: SectionTitle): Boolean = this === other
notice(R.string.remove_file_deleted) 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)
}
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
} }
} }
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)
} }
when {
item.updated -> notice(R.string.update_file_created) @get:Bindable
item.remove -> notice(R.string.remove_file_created) 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)
} }
fun toggle() = isChecked.toggle() val isUpdated get() = item.updated
fun toggleDelete() = isDeletable.toggle() val isModified get() = isRemoved || item.updated
private fun notice(@StringRes info: Int) { fun toggle() {
lastActionNotice.value = get<Resources>().getString(info) isEnabled = !isEnabled
} }
override fun contentSameAs(other: ModuleRvItem): Boolean = item.version == other.item.version fun delete(viewModel: ModuleViewModel) {
isRemoved = !isRemoved
viewModel.updateActiveState()
}
override fun contentSameAs(other: ModuleItem): Boolean = item.version == other.item.version
&& item.versionCode == other.item.versionCode && item.versionCode == other.item.versionCode
&& item.description == other.item.description && item.description == other.item.description
&& item.name == other.item.name && 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 {
private val list = PropertyChangeRegistry()
override val layoutRes: Int = R.layout.item_repo override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
list.remove(callback ?: return)
}
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
list.add(callback ?: return)
}
override fun contentSameAs(other: RepoRvItem): Boolean = item == other.item fun notifyChange(id: Int) = list.notifyChange(this, id)
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
} }
...@@ -2,51 +2,52 @@ package com.topjohnwu.magisk.model.entity.recycler ...@@ -2,51 +2,52 @@ package com.topjohnwu.magisk.model.entity.recycler
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.model.MagiskPolicy
import com.topjohnwu.magisk.databinding.ComparableRvItem 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.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.model.events.PolicyUpdateEvent
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.utils.KObservableField import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() { class PolicyItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyItem>() {
override val layoutRes = R.layout.item_policy_md2
override val layoutRes: Int = R.layout.item_policy
val isExpanded = KObservableField(false) val isExpanded = KObservableField(false)
val isEnabled = KObservableField(item.policy == MagiskPolicy.ALLOW) val isEnabled = KObservableField(item.policy == MagiskPolicy.ALLOW)
val shouldNotify = KObservableField(item.notification) val shouldNotify = KObservableField(item.notification)
val shouldLog = KObservableField(item.logging) val shouldLog = KObservableField(item.logging)
fun toggle() = isExpanded.toggle() private val updatedPolicy
private val rxBus: RxBus by inject()
private val currentStateItem
get() = item.copy( get() = item.copy(
policy = if (isEnabled.value) MagiskPolicy.ALLOW else MagiskPolicy.DENY, policy = if (isEnabled.value) MagiskPolicy.ALLOW else MagiskPolicy.DENY,
notification = shouldNotify.value, notification = shouldNotify.value,
logging = shouldLog.value logging = shouldLog.value
) )
init { fun toggle(viewModel: SuperuserViewModel) {
isEnabled.addOnPropertyChangedCallback { if (isExpanded.value) {
it ?: return@addOnPropertyChangedCallback toggle()
rxBus.post(PolicyEnableEvent(this@PolicyRvItem, it)) return
}
isEnabled.toggle()
viewModel.togglePolicy(this, isEnabled.value)
} }
shouldNotify.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback fun toggle() {
rxBus.post(PolicyUpdateEvent.Notification(currentStateItem)) isExpanded.toggle()
} }
shouldLog.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback fun toggleNotify(viewModel: SuperuserViewModel) {
rxBus.post(PolicyUpdateEvent.Log(currentStateItem)) shouldNotify.toggle()
viewModel.updatePolicy(PolicyUpdateEvent.Notification(updatedPolicy))
} }
fun toggleLog(viewModel: SuperuserViewModel) {
shouldLog.toggle()
viewModel.updatePolicy(PolicyUpdateEvent.Log(updatedPolicy))
} }
override fun contentSameAs(other: PolicyRvItem): Boolean = itemSameAs(other) override fun contentSameAs(other: PolicyItem) = itemSameAs(other)
override fun itemSameAs(other: PolicyRvItem): Boolean = item.uid == other.item.uid 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 package com.topjohnwu.magisk.model.events
import com.topjohnwu.magisk.model.entity.MagiskPolicy import com.topjohnwu.magisk.core.model.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.utils.RxBus 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 { sealed class PolicyUpdateEvent(val item: MagiskPolicy) : RxBus.Event {
class Notification(item: MagiskPolicy) : PolicyUpdateEvent(item) class Notification(item: MagiskPolicy) : PolicyUpdateEvent(item)
class Log(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 ...@@ -3,13 +3,16 @@ package com.topjohnwu.magisk.model.events
import android.content.Context import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.google.android.material.snackbar.Snackbar 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( class SnackbarEvent private constructor(
@StringRes private val messageRes: Int, @StringRes private val messageRes: Int,
private val messageString: String?, private val messageString: String?,
val length: Int, val length: Int,
val f: Snackbar.() -> Unit val f: Snackbar.() -> Unit
) : ViewEvent() { ) : ViewEvent(), ActivityExecutor {
constructor( constructor(
@StringRes messageRes: Int, @StringRes messageRes: Int,
...@@ -24,4 +27,11 @@ class SnackbarEvent private constructor( ...@@ -24,4 +27,11 @@ class SnackbarEvent private constructor(
) : this(-1, message, length, f) ) : this(-1, message, length, f)
fun message(context: Context): String = messageString ?: context.getString(messageRes) 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 package com.topjohnwu.magisk.model.events
import com.topjohnwu.magisk.base.BaseActivity import android.app.Activity
import com.topjohnwu.magisk.model.entity.module.Repo 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 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 * Class for passing events from ViewModels to Activities/Fragments
...@@ -15,33 +35,147 @@ abstract class ViewEvent { ...@@ -15,33 +35,147 @@ abstract class ViewEvent {
var handled = false var handled = false
} }
data class OpenLinkEvent(val url: String) : ViewEvent() class UpdateSafetyNetEvent : ViewEvent(), ContextExecutor, KoinComponent, SafetyNetHelper.Callback {
class ManagerInstallEvent : ViewEvent() private val magiskRepo by inject<MagiskRepository>()
class MagiskInstallEvent : ViewEvent() private val rxBus by inject<RxBus>()
class ManagerChangelogEvent : ViewEvent() private lateinit var EXT_APK: File
class MagiskChangelogEvent : ViewEvent() private lateinit var EXT_DEX: File
class UninstallEvent : ViewEvent() override fun invoke(context: Context) {
class EnvFixEvent : ViewEvent() 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() val helper = helperClass.getMethod(
class InstallModuleEvent(val item: Repo) : ViewEvent() "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( class PermissionEvent(
val permissions: List<String>, val permissions: List<String>,
val callback: PublishSubject<Boolean> 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 ...@@ -3,8 +3,8 @@ package com.topjohnwu.magisk.model.flash
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import com.topjohnwu.magisk.core.tasks.FlashZip
import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.tasks.FlashZip
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
......
package com.topjohnwu.magisk.model.flash package com.topjohnwu.magisk.model.flash
import android.net.Uri import android.net.Uri
import com.topjohnwu.magisk.tasks.MagiskInstaller import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
sealed class Patching( sealed class Patching(
...@@ -28,8 +28,7 @@ sealed class Patching( ...@@ -28,8 +28,7 @@ sealed class Patching(
logs: MutableList<String>, logs: MutableList<String>,
resultListener: FlashResultListener resultListener: FlashResultListener
) : Patching(file, console, logs, resultListener) { ) : Patching(file, console, logs, resultListener) {
override fun operations() = override fun operations() = doPatchFile(uri)
extractZip() && handleFile(uri) && patchBoot() && storeBoot()
} }
class SecondSlot( class SecondSlot(
...@@ -38,8 +37,7 @@ sealed class Patching( ...@@ -38,8 +37,7 @@ sealed class Patching(
logs: MutableList<String>, logs: MutableList<String>,
resultListener: FlashResultListener resultListener: FlashResultListener
) : Patching(file, console, logs, resultListener) { ) : Patching(file, console, logs, resultListener) {
override fun operations() = override fun operations() = secondSlot()
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
} }
class Direct( class Direct(
...@@ -48,8 +46,7 @@ sealed class Patching( ...@@ -48,8 +46,7 @@ sealed class Patching(
logs: MutableList<String>, logs: MutableList<String>,
resultListener: FlashResultListener resultListener: FlashResultListener
) : Patching(file, console, logs, resultListener) { ) : Patching(file, console, logs, resultListener) {
override fun operations() = override fun operations() = direct()
findImage() && extractZip() && patchBoot() && flashBoot()
} }
} }
...@@ -4,7 +4,10 @@ import android.os.Bundle ...@@ -4,7 +4,10 @@ import android.os.Bundle
import androidx.annotation.AnimRes import androidx.annotation.AnimRes
import androidx.annotation.AnimatorRes import androidx.annotation.AnimatorRes
import androidx.fragment.app.Fragment 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.model.events.ViewEvent
import com.topjohnwu.magisk.ui.base.CompatActivity
import kotlin.reflect.KClass import kotlin.reflect.KClass
@DslMarker @DslMarker
...@@ -14,12 +17,17 @@ class MagiskNavigationEvent( ...@@ -14,12 +17,17 @@ class MagiskNavigationEvent(
val navDirections: MagiskNavDirectionsBuilder, val navDirections: MagiskNavDirectionsBuilder,
val navOptions: MagiskNavOptions, val navOptions: MagiskNavOptions,
val animOptions: MagiskAnimBuilder val animOptions: MagiskAnimBuilder
) : ViewEvent() { ) : ViewEvent(), ActivityExecutor {
companion object { companion object {
operator fun invoke(builder: Builder.() -> Unit) = Builder().apply(builder).build() 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 @NavigationDslMarker
class Builder { class Builder {
......
package com.topjohnwu.magisk.model.navigation 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.home.HomeFragment
import com.topjohnwu.magisk.ui.install.InstallFragment
import com.topjohnwu.magisk.ui.log.LogFragment import com.topjohnwu.magisk.ui.log.LogFragment
import com.topjohnwu.magisk.ui.module.ModulesFragment import com.topjohnwu.magisk.ui.module.ModuleFragment
import com.topjohnwu.magisk.ui.module.ReposFragment import com.topjohnwu.magisk.ui.safetynet.SafetynetFragment
import com.topjohnwu.magisk.ui.settings.SettingsFragment import com.topjohnwu.magisk.ui.settings.SettingsFragment
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment import com.topjohnwu.magisk.ui.superuser.SuperuserFragment
import com.topjohnwu.magisk.ui.theme.ThemeFragment
object Navigation { object Navigation {
fun home() = MagiskNavigationEvent { fun home() = MagiskNavigationEvent {
navDirections { destination = HomeFragment::class } navDirections {
navOptions { popUpTo = HomeFragment::class } destination = HomeFragment::class
}
navOptions {
popUpTo = HomeFragment::class
}
} }
fun superuser() = MagiskNavigationEvent { fun superuser() = MagiskNavigationEvent {
navDirections { destination = SuperuserFragment::class } navDirections {
destination = SuperuserFragment::class
}
} }
fun modules() = MagiskNavigationEvent { fun modules() = MagiskNavigationEvent {
navDirections { destination = ModulesFragment::class } navDirections {
destination = ModuleFragment::class
} }
fun repos() = MagiskNavigationEvent {
navDirections { destination = ReposFragment::class }
} }
fun hide() = MagiskNavigationEvent { fun hide() = MagiskNavigationEvent {
navDirections { destination = MagiskHideFragment::class } navDirections {
destination = HideFragment::class
}
}
fun safetynet() = MagiskNavigationEvent {
navDirections { destination = SafetynetFragment::class }
} }
fun log() = MagiskNavigationEvent { fun log() = MagiskNavigationEvent {
navDirections { destination = LogFragment::class } navDirections {
destination = LogFragment::class
}
} }
fun settings() = MagiskNavigationEvent { 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) { fun fromSection(section: String) = when (section) {
"superuser" -> superuser() "superuser" -> superuser()
"modules" -> modules() "modules" -> modules()
"downloads" -> repos()
"magiskhide" -> hide() "magiskhide" -> hide()
"log" -> log() "log" -> log()
"settings" -> settings() "settings" -> settings()
else -> home() 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 { object Main {
const val OPEN_NAV = 1 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 { ...@@ -8,6 +8,4 @@ interface Navigator {
//TODO Elevate Fragment to MagiskFragment<*,*> once everything is on board with it //TODO Elevate Fragment to MagiskFragment<*,*> once everything is on board with it
val baseFragments: List<KClass<out Fragment>> val baseFragments: List<KClass<out Fragment>>
fun navigateTo(event: MagiskNavigationEvent)
} }
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 package com.topjohnwu.magisk.ui
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog import android.view.MenuItem
import androidx.core.view.GravityCompat 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.Fragment
import androidx.fragment.app.FragmentTransaction import com.google.android.material.card.MaterialCardView
import com.ncapdevi.fragnav.FragNavController 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.R
import com.topjohnwu.magisk.base.BaseActivity import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.base.BaseFragment import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
import com.topjohnwu.magisk.databinding.ActivityMainBinding import com.topjohnwu.magisk.extensions.startAnimations
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.model.navigation.Navigation import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.model.navigation.Navigator import com.topjohnwu.magisk.ui.base.CompatActivity
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment import com.topjohnwu.magisk.ui.base.CompatNavigationDelegate
import com.topjohnwu.magisk.ui.home.HomeFragment import com.topjohnwu.magisk.ui.home.HomeFragment
import com.topjohnwu.magisk.ui.log.LogFragment import com.topjohnwu.magisk.ui.module.ModuleFragment
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.superuser.SuperuserFragment 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 org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
import kotlin.reflect.KClass import kotlin.reflect.KClass
open class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>(), Navigator, open class MainActivity : CompatActivity<MainViewModel, ActivityMainMd2Binding>(),
FragNavController.RootFragmentListener, FragNavController.TransactionListener { FragNavController.TransactionListener {
override val layoutRes: Int = R.layout.activity_main override val layoutRes = R.layout.activity_main_md2
override val viewModel: MainViewModel by viewModel() override val viewModel by viewModel<MainViewModel>()
private val navHostId: Int = R.id.main_nav_host override val navHost: Int = R.id.main_nav_host
private val defaultPosition: Int = 0
private val navigationController by lazy { override val navigation by lazy { CompatNavigationDelegate(this, this) }
FragNavController(supportFragmentManager, navHostId)
}
private val isRootFragment get() =
navigationController.currentStackIndex != defaultPosition
override val baseFragments: List<KClass<out Fragment>> = listOf( override val baseFragments: List<KClass<out Fragment>> = listOf(
HomeFragment::class, HomeFragment::class,
SuperuserFragment::class, ModuleFragment::class,
MagiskHideFragment::class, SuperuserFragment::class
ModulesFragment::class,
ReposFragment::class,
LogFragment::class,
SettingsFragment::class
) )
override fun onCreate(savedInstanceState: Bundle?) { //This temporarily fixes unwanted feature of BottomNavigationView - where the view applies
if (!SplashActivity.DONE) { //padding on itself given insets are not consumed beforehand. Unfortunately the listener
startActivity(intent<SplashActivity>()) //implementation doesn't favor us against the design library, so on re-create it's often given
finish() //upper hand.
private val navObserver = ViewTreeObserver.OnGlobalLayoutListener {
binding.mainNavigation.setPadding(0)
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (Info.env.isUnsupported && !viewModel.shownUnsupportedDialog) { window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
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 { setSupportActionBar(binding.mainToolbar)
rootFragmentListener = this@MainActivity
transactionListener = this@MainActivity
initialize(defaultPosition, savedInstanceState)
}
checkHideSection() binding.mainToolbarWrapper.updateLayoutParams<CoordinatorLayout.LayoutParams> {
setSupportActionBar(binding.mainInclude.mainToolbar) behavior = HideTopViewOnScrollBehavior<MaterialCardView>()
viewModel.isConnected.addOnPropertyChangedCallback {
checkHideSection()
}
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 {
override fun onSaveInstanceState(outState: Bundle) { navigation.onReselected()
super.onSaveInstanceState(outState)
navigationController.onSaveInstanceState(outState)
} }
override fun setTitle(title: CharSequence?) { binding.mainNavigation.viewTreeObserver.addOnGlobalLayoutListener(navObserver)
supportActionBar?.title = title
}
override fun setTitle(titleId: Int) { if (intent.getBooleanExtra(Const.Key.OPEN_SETTINGS, false)) {
supportActionBar?.setTitle(titleId) Navigation.settings().dispatchOnSelf()
} }
override fun onBackPressed() { if (savedInstanceState != null) {
if (binding.drawerLayout.isDrawerOpen(binding.navView)) { onTabTransaction(null, -1)
binding.drawerLayout.closeDrawer(binding.navView)
} else {
val fragment = navigationController.currentFrag as? BaseFragment<*, *>
if (fragment?.onBackPressed() == true) {
return
}
try { if (!navigation.isRoot) {
navigationController.popFragment() requestNavigationHidden()
} catch (e: UnsupportedOperationException) {
when {
isRootFragment -> {
val options = FragNavTransactionOptions.newBuilder()
.transition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
.build()
navigationController.switchTab(defaultPosition, options)
}
else -> super.onBackPressed()
}
} }
} }
} }
override fun onEventDispatched(event: ViewEvent) { override fun onResume() {
super.onEventDispatched(event) super.onResume()
when (event) { binding.mainNavigation.menu.apply {
is SnackbarEvent -> snackbar(snackbarView, event.message(this), event.length, event.f) val isRoot = Shell.rootAccess()
is BackPressEvent -> onBackPressed() findItem(R.id.modulesFragment)?.isEnabled = isRoot
is MagiskNavigationEvent -> navigateTo(event) findItem(R.id.superuserFragment)?.isEnabled = isRoot
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 onDestroy() {
binding.mainNavigation.viewTreeObserver.removeOnGlobalLayoutListener(navObserver)
super.onDestroy()
} }
override fun onSimpleEventDispatched(event: Int) { override fun onOptionsItemSelected(item: MenuItem): Boolean {
super.onSimpleEventDispatched(event) when (item.itemId) {
when (event) { android.R.id.home -> onBackPressed()
Navigation.Main.OPEN_NAV -> openNav() else -> return super.onOptionsItemSelected(item)
} }
return true
} }
private fun openNav() = binding.drawerLayout.openDrawer(GravityCompat.START) override fun onTabTransaction(fragment: Fragment?, index: Int) =
onFragmentTransaction(fragment, FragNavController.TransactionType.PUSH)
private fun checkHideSection() { override fun onFragmentTransaction(
val menu = binding.navView.menu fragment: Fragment?,
menu.findItem(R.id.magiskHideFragment).isVisible = Info.env.isActive && Info.env.magiskHide transactionType: FragNavController.TransactionType
menu.findItem(R.id.modulesFragment).isVisible = Info.env.isActive ) {
menu.findItem(R.id.reposFragment).isVisible = Info.isConnected.value && Info.env.isActive setDisplayHomeAsUpEnabled(!navigation.isRoot)
menu.findItem(R.id.logFragment).isVisible = Info.env.isActive requestNavigationHidden(!navigation.isRoot)
menu.findItem(R.id.superuserFragment).isVisible = Utils.showSuperUser()
} }
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) = override fun peekSystemWindowInsets(insets: Insets) {
customAnimations(options.enter, options.exit, options.popEnter, options.popExit).apply { viewModel.insets.value = insets
if (!options.anySet) {
transition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
}
} }
override val numberOfRootFragments: Int get() = baseFragments.size private fun setDisplayHomeAsUpEnabled(isEnabled: Boolean) {
binding.mainToolbar.startAnimations()
override fun getRootFragment(index: Int) = baseFragments[index].java.newInstance() when {
isEnabled -> binding.mainToolbar.setNavigationIcon(R.drawable.ic_back_md2)
override fun onTabTransaction(fragment: Fragment?, index: Int) { else -> binding.mainToolbar.navigationIcon = null
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
} }
binding.navView.setCheckedItem(fragmentId)
} }
override fun navigateTo(event: MagiskNavigationEvent) { @Suppress("UNCHECKED_CAST")
val directions = event.navDirections internal fun requestNavigationHidden(hide: Boolean = true) {
val topView = binding.mainToolbarWrapper
navigationController.defaultTransactionOptions = FragNavTransactionOptions.newBuilder() val bottomView = binding.mainBottomBar
.customAnimations(event.animOptions)
.build()
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) }
when (directions.isActivity) {
true -> navigateToActivity(event)
else -> navigateToFragment(event)
}
}
private fun navigateToActivity(event: MagiskNavigationEvent) { val topParams = topView.layoutParams as? CoordinatorLayout.LayoutParams
val destination = event.navDirections.destination?.java ?: let { val bottomParams = bottomView.layoutParams as? CoordinatorLayout.LayoutParams
Timber.e("Cannot navigate to null destination")
return
}
val options = event.navOptions
Intent(this, destination) val topBehavior = topParams?.behavior as? HideableBehavior<View>
.putExtras(event.navDirections.args) val bottomBehavior = bottomParams?.behavior as? HideableBehavior<View>
.apply {
if (options.singleTop) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
if (options.clearTask) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
.let { startActivity(it) }
}
private fun navigateToFragment(event: MagiskNavigationEvent) { topBehavior?.setHidden(topView, hide = false, lockState = false)
val destination = event.navDirections.destination?.java ?: let { bottomBehavior?.setHidden(bottomView, hide, hide)
Timber.e("Cannot navigate to null destination")
return
} }
when (val index = baseFragments.indexOfFirst { it.java.name == destination.name }) { fun invalidateToolbar() {
-1 -> destination.newInstance() //binding.mainToolbar.startAnimations()
.apply { arguments = event.navDirections.args } binding.mainToolbar.invalidate()
.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 package com.topjohnwu.magisk.ui
import android.view.MenuItem import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.model.navigation.Navigation
class MainViewModel : 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
}
}
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.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.BR 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.EventHandler
import com.topjohnwu.magisk.model.events.ViewEvent import com.topjohnwu.magisk.model.events.ViewEvent
abstract class BaseFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding> : abstract class BaseUIFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
Fragment(), EventHandler { Fragment(), EventHandler {
protected val activity get() = requireActivity() as BaseActivity<*, *> protected val activity get() = requireActivity() as BaseUIActivity<*, *>
protected lateinit var binding: Binding protected lateinit var binding: Binding
protected abstract val layoutRes: Int protected abstract val layoutRes: Int
protected abstract val viewModel: ViewModel abstract val viewModel: ViewModel
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
...@@ -33,13 +31,12 @@ abstract class BaseFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding ...@@ -33,13 +31,12 @@ abstract class BaseFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding
): View? { ): View? {
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).apply { binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).apply {
setVariable(BR.viewModel, viewModel) setVariable(BR.viewModel, viewModel)
lifecycleOwner = this@BaseFragment lifecycleOwner = this@BaseUIFragment
} }
return binding.root return binding.root
} }
@CallSuper
override fun onEventDispatched(event: ViewEvent) { override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event) super.onEventDispatched(event)
activity.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 package com.topjohnwu.magisk.ui.flash
import android.Manifest.permission.READ_EXTERNAL_STORAGE import com.topjohnwu.magisk.ui.base.BaseViewModel
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.*
class FlashViewModel( class FlashViewModel : BaseViewModel()
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
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 package com.topjohnwu.magisk.ui.hide
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR 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.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.subscribeK
import com.topjohnwu.magisk.extensions.toSingle import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.extensions.update import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.magisk.model.entity.recycler.HideRvItem import com.topjohnwu.magisk.model.entity.ProcessHideApp
import com.topjohnwu.magisk.model.entity.state.IndeterminateState import com.topjohnwu.magisk.model.entity.StatefulProcess
import com.topjohnwu.magisk.model.events.HideProcessEvent import com.topjohnwu.magisk.model.entity.recycler.HideItem
import com.topjohnwu.magisk.utils.DiffObservableList 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.KObservableField
import com.topjohnwu.magisk.utils.RxBus
import me.tatarka.bindingcollectionadapter2.OnItemBind
import timber.log.Timber
class HideViewModel( class HideViewModel(
private val magiskRepo: MagiskRepository, private val magiskRepo: MagiskRepository
rxBus: RxBus ) : BaseViewModel(), Queryable by Queryable.impl(1000) {
) : 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)
}
init { override val queryRunnable = Runnable { query() }
rxBus.register<HideProcessEvent>()
.subscribeK { toggleItem(it.item) }
.add()
isShowSystem.addOnPropertyChangedCallback { query() } var isShowSystem = false
query.addOnPropertyChangedCallback { query() } @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)
}
val itemInternalBinding = itemBindingOf<HideProcessItem> {
it.bindExtra(BR.viewModel, this)
} }
fun refresh() { val isFilterExpanded = KObservableField(false)
// 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() override fun refresh() = magiskRepo.fetchApps()
.map { it to magiskRepo.fetchHideTargets().blockingGet() }
.map { pair -> pair.first.map { mergeAppTargets(it, pair.second) } }
.flattenAsFlowable { it } .flattenAsFlowable { it }
.map { HideRvItem(it, hideTargets.blockingGet()) } .map { HideItem(it) }
.toList() .toList()
.map { .map { it.sort() }
it.sortedWith(compareBy( .map { it to items.calculateDiff(it) }
{ it.isHiddenState.value },
{ it.item.name.toLowerCase() },
{ it.packageName }
))
}
.doOnSuccess { allItems.update(it) }
.flatMap { queryRaw() }
.applyViewModel(this) .applyViewModel(this)
.subscribeK(onError = Timber::e) { items.update(it.first, it.second) } .subscribeK {
.add() items.update(it.first, it.second)
submitQuery()
} }
private fun query() = queryRaw() // ---
.subscribeK { items.update(it.first, it.second) }
.add()
private fun queryRaw( private fun mergeAppTargets(a: HideAppInfo, ts: List<HideTarget>): ProcessHideApp {
showSystem: Boolean = isShowSystem.value, val relevantTargets = ts.filter { it.packageName == a.info.packageName }
query: String = this.query.value val packageName = a.info.packageName
) = allItems.toSingle() val processes = a.processes
.map { it.filterIsInstance<HideRvItem>() } .map { StatefulProcess(it, packageName, relevantTargets.any { i -> it == i.process }) }
.flattenAsFlowable { it } return ProcessHideApp(a, processes)
.filter {
it.item.name.contains(query, ignoreCase = true) ||
it.item.processes.any { it.contains(query, ignoreCase = true) }
} }
.filter {
showSystem || (it.isHiddenState.value != IndeterminateState.UNCHECKED) || private fun List<HideItem>.sort() = compareByDescending<HideItem> { it.itemsChecked.value }
(it.item.info.flags and ApplicationInfo.FLAG_SYSTEM == 0) .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)
} }
.toList()
.map { it to items.calculateDiff(it) }
private fun toggleItem(item: HideProcessRvItem) = private fun query(
magiskRepo.toggleHide(item.isHidden.value, item.packageName, item.process) 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 package com.topjohnwu.magisk.ui.home
import android.content.Context import android.view.Menu
import com.topjohnwu.magisk.BuildConfig import android.view.MenuInflater
import com.topjohnwu.magisk.Const import android.view.MenuItem
import com.topjohnwu.magisk.Info import androidx.core.graphics.Insets
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
import com.topjohnwu.magisk.base.BaseFragment import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.data.repository.MagiskRepository import com.topjohnwu.magisk.ui.base.CompatFragment
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 org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import java.io.File
import java.lang.reflect.InvocationHandler
class HomeFragment : BaseFragment<HomeViewModel, FragmentMagiskBinding>(), class HomeFragment : CompatFragment<HomeViewModel, FragmentHomeMd2Binding>() {
SafetyNetHelper.Callback {
override val layoutRes: Int = R.layout.fragment_magisk override val layoutRes = R.layout.fragment_home_md2
override val viewModel: HomeViewModel by viewModel() override val viewModel by viewModel<HomeViewModel>()
private val magiskRepo: MagiskRepository by inject() override fun consumeSystemWindowInsets(insets: Insets) = insets
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 onStart() { override fun onStart() {
super.onStart() super.onStart()
activity.title = resources.getString(R.string.section_home)
setHasOptionsMenu(true) 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()
}
private fun installManager() = ManagerInstallDialog(requireActivity()).show()
private fun uninstall() = UninstallDialog(requireActivity()).show()
private fun fixEnv() = EnvFixDialog(requireActivity()).show()
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()) override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
.setTitle(R.string.proprietary_title) inflater.inflate(R.menu.menu_home_md2, menu)
.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) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
Completable.fromAction { R.id.action_settings -> Navigation.settings().dispatchOnSelf()
val loader = DynamicClassLoader(EXT_APK) else -> null
val dex = DexFile.loadDex(EXT_APK.path, EXT_DEX.path, 0) }?.let { true } ?: super.onOptionsItemSelected(item)
// 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 package com.topjohnwu.magisk.ui.home
import android.content.pm.PackageManager import android.Manifest
import com.topjohnwu.magisk.* import android.os.Build
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel 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.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.* 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.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.KObservableField
import com.topjohnwu.magisk.utils.SafetyNetHelper
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import io.reactivex.Completable import me.tatarka.bindingcollectionadapter2.BR
import kotlin.math.roundToInt
enum class SafetyNetState {
LOADING, PASS, FAILED, IDLE
}
enum class MagiskState { enum class MagiskState {
NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING
} }
enum class MagiskItem {
MANAGER, MAGISK
}
class HomeViewModel( class HomeViewModel(
private val magiskRepo: MagiskRepository private val repoMagisk: MagiskRepository
) : BaseViewModel(State.LOADED) { ) : BaseViewModel() {
val hasGMS = runCatching {
get<PackageManager>().getPackageInfo("com.google.android.gms", 0); true
}.getOrElse { false }
val isAdvancedExpanded = KObservableField(false)
val isForceEncryption = KObservableField(Info.keepEnc) val isNoticeVisible = KObservableField(Config.safetyNotice)
val isKeepVerity = KObservableField(Info.keepVerity)
val isRecovery = KObservableField(Info.recovery)
val magiskState = KObservableField(MagiskState.LOADING) val stateMagisk = KObservableField(MagiskState.LOADING)
val magiskStateText = Observer(magiskState) { val stateManager = KObservableField(MagiskState.LOADING)
when (magiskState.value) { val stateTextMagisk = Observer(stateMagisk) {
MagiskState.NOT_INSTALLED -> R.string.magisk_version_error.res() when (stateMagisk.value) {
MagiskState.UP_TO_DATE -> R.string.magisk_up_to_date.res() MagiskState.NOT_INSTALLED -> R.string.installed_error_md2.res()
MagiskState.LOADING -> R.string.checking_for_updates.res() MagiskState.UP_TO_DATE -> R.string.up_to_date_md2.res()
MagiskState.OBSOLETE -> R.string.magisk_update_title.res() MagiskState.LOADING -> R.string.loading_md2.res()
MagiskState.OBSOLETE -> R.string.obsolete_md2.res()
} }
} }
val magiskCurrentVersion = KObservableField("") val stateTextManager = Observer(stateManager) {
val magiskLatestVersion = KObservableField("") when (stateManager.value) {
val magiskAdditionalInfo = Observer(magiskState) { MagiskState.NOT_INSTALLED -> R.string.channel_error_md2.res()
if (Config.coreOnly) MagiskState.UP_TO_DATE -> R.string.up_to_date_md2.res()
R.string.core_only_enabled.res() MagiskState.LOADING -> R.string.loading_md2.res()
else MagiskState.OBSOLETE -> R.string.obsolete_md2.res()
""
}
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 managerCurrentVersion = KObservableField("") val statePackageManager = packageName
val managerLatestVersion = KObservableField("") val statePackageOriginal = statePackageManager == BuildConfig.APPLICATION_ID
val managerAdditionalInfo = Observer(managerState) { val stateVersionUpdateMagisk = KObservableField("")
if (packageName != BuildConfig.APPLICATION_ID) val stateVersionUpdateManager = KObservableField("")
"($packageName)"
else val stateMagiskProgress = KObservableField(0)
"" val stateManagerProgress = KObservableField(0)
}
val safetyNetTitle = KObservableField(R.string.safetyNet_check_text.res()) val stateMagiskExpanded = KObservableField(false)
val ctsState = KObservableField(SafetyNetState.IDLE) val stateManagerExpanded = KObservableField(false)
val basicIntegrityState = KObservableField(SafetyNetState.IDLE)
val safetyNetState = Observer(ctsState, basicIntegrityState) {
val cts = ctsState.value
val basic = basicIntegrityState.value
val states = listOf(cts, basic)
when { val stateHideManagerName = R.string.manager.res().let {
states.any { it == SafetyNetState.LOADING } -> State.LOADING if (!statePackageOriginal) {
states.any { it == SafetyNetState.IDLE } -> State.LOADING it.replaceRandomWithSpecial(3)
else -> State.LOADED } else {
it
} }
} }
val isActive = KObservableField(false) val items = listOf(DeveloperItem.Mainline, DeveloperItem.App, DeveloperItem.Project)
val itemBinding = itemBindingOf<HomeItem> {
it.bindExtra(BR.viewModel, this)
}
val itemDeveloperBinding = itemBindingOf<DeveloperItem> {
it.bindExtra(BR.viewModel, this)
}
private var shownDialog = false private var shownDialog = false
init { init {
isForceEncryption.addOnPropertyChangedCallback { RemoteFileService.progressBroadcast.observeForever {
Info.keepEnc = it ?: return@addOnPropertyChangedCallback when (it?.second) {
} is Magisk.Download,
isKeepVerity.addOnPropertyChangedCallback { is Magisk.Flash -> stateMagiskProgress.value = it.first.times(100f).roundToInt()
Info.keepVerity = it ?: return@addOnPropertyChangedCallback is Manager -> stateManagerProgress.value = it.first.times(100f).roundToInt()
}
isRecovery.addOnPropertyChangedCallback {
Info.recovery = it ?: return@addOnPropertyChangedCallback
} }
isConnected.addOnPropertyChangedCallback {
if (it == true) refresh(false)
} }
refresh(false)
} }
fun paypalPressed() = OpenLinkEvent(Const.Url.PAYPAL_URL).publish() override fun refresh() = repoMagisk.fetchUpdate()
fun patreonPressed() = OpenLinkEvent(Const.Url.PATREON_URL).publish() .onErrorReturn { Info.remote }
fun twitterPressed() = OpenLinkEvent(Const.Url.TWITTER_URL).publish() .subscribeK { updateBy(it) }
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) { private fun updateBy(info: UpdateInfo) {
MagiskItem.MANAGER -> ManagerInstallEvent().publish() stateMagisk.value = when {
MagiskItem.MAGISK -> MagiskInstallEvent().publish() !info.magisk.isInstalled -> MagiskState.NOT_INSTALLED
info.magisk.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
} }
fun cardPressed(item: MagiskItem) = when (item) { stateManager.value = when {
MagiskItem.MANAGER -> ManagerChangelogEvent().publish() !info.app.isUpdateChannelCorrect && isConnected.value -> MagiskState.NOT_INSTALLED
MagiskItem.MAGISK -> MagiskChangelogEvent().publish() info.app.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
} }
fun safetyNetPressed() { stateVersionUpdateMagisk.value = when {
ctsState.value = SafetyNetState.LOADING info.magisk.isObsolete -> "%s > %s".format(
basicIntegrityState.value = SafetyNetState.LOADING Info.env.magiskVersionString.clipVersion(info.magisk.version),
safetyNetTitle.value = R.string.checking_safetyNet_status.res() info.magisk.version.clipVersion(Info.env.magiskVersionString)
)
UpdateSafetyNetEvent().publish() else -> ""
} }
fun finishSafetyNetCheck(response: Int) = when { stateVersionUpdateManager.value = when {
response and 0x0F == 0 -> { info.app.isObsolete -> "%s > %s".format(
val hasCtsPassed = response and SafetyNetHelper.CTS_PASS != 0 BuildConfig.VERSION_NAME.clipVersion(info.app.version),
val hasBasicIntegrityPassed = response and SafetyNetHelper.BASIC_PASS != 0 info.app.version.clipVersion(BuildConfig.VERSION_NAME)
safetyNetTitle.value = R.string.safetyNet_check_success.res() )
ctsState.value = if (hasCtsPassed) { else -> ""
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()
} }
}
}
@JvmOverloads
fun refresh(invalidate: Boolean = true) {
if (invalidate)
Info.envRef.invalidate()
isActive.value = Info.env.isActive
val fetchUpdate = if (isConnected.value)
magiskRepo.fetchUpdate().ignoreElement()
else
Completable.complete()
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() ensureEnv()
refreshVersions()
}
} }
private fun refreshVersions() { fun onLinkPressed(link: String) = OpenInappLinkEvent(link).publish()
magiskCurrentVersion.value = if (magiskState.value != MagiskState.NOT_INSTALLED) {
VERSION_FMT.format(Info.env.magiskVersionString, Info.env.magiskVersionCode)
} else {
""
}
managerCurrentVersion.value = if (isRunningAsStub) MGR_VER_FMT fun onDeletePressed() = UninstallDialog().publish()
.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, Info.stub!!.version)
else
VERSION_FMT.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
}
private fun updateSelf() { fun onManagerPressed() = ManagerInstallDialog().publish()
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
}
magiskLatestVersion.value = fun onMagiskPressed() = withPermissions(
VERSION_FMT.format(Info.remote.magisk.version, Info.remote.magisk.versionCode) Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
).map { check(it);it }
.subscribeK { Navigation.install().publish() }
.add()
_managerState.value = when (Info.remote.app.versionCode) { fun toggle(kof: KObservableField<Boolean>) = kof.toggle()
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
}
}
managerLatestVersion.value = MGR_VER_FMT fun hideNotice() {
.format(Info.remote.app.version, Info.remote.app.versionCode, Info.remote.stub.versionCode) Config.safetyNotice = false
isNoticeVisible.value = false
} }
private fun ensureEnv() { private fun ensureEnv() {
val invalidStates = val invalidStates = listOf(
listOf(MagiskState.NOT_INSTALLED, MagiskState.LOADING) MagiskState.NOT_INSTALLED,
MagiskState.LOADING
)
// Don't bother checking env when magisk is not installed, loading or already has been shown // 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 (
invalidStates.any { it == stateMagisk.value } ||
if (!Shell.su("env_check").exec().isSuccess) { 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 shownDialog = true
EnvFixEvent().publish() EnvFixDialog().publish()
} }
} }
companion object { private fun String.clipVersion(other: String = ""): String {
private const val VERSION_FMT = "%s (%d)" val thisVersion = substringBefore('-')
private const val MGR_VER_FMT = "%s (%d) (%d)" 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 package com.topjohnwu.magisk.ui.log
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding
import com.topjohnwu.magisk.databinding.FragmentLogBinding import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.model.events.PageChangedEvent import com.topjohnwu.magisk.ui.base.CompatFragment
import com.topjohnwu.magisk.model.events.ViewEvent import com.topjohnwu.magisk.utils.MotionRevealHelper
import org.koin.androidx.viewmodel.ext.android.viewModel 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 layoutRes = R.layout.fragment_log_md2
override val viewModel: LogViewModel by viewModel() override val viewModel by viewModel<LogViewModel>()
override fun onEventDispatched(event: ViewEvent) { private var actionSave: MenuItem? = null
super.onEventDispatched(event) private var isMagiskLogVisible
when (event) { get() = binding.logFilter.isVisible
is PageChangedEvent -> activity.invalidateOptionsMenu() set(value) {
} MotionRevealHelper.withViews(binding.logFilter, binding.logFilterToggle, value)
actionSave?.isVisible = value
(activity as MainActivity).invalidateToolbar()
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun consumeSystemWindowInsets(insets: Insets) = insets
super.onViewCreated(view, savedInstanceState)
binding.logTabs.setupWithViewPager(binding.logContainer, true)
}
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
setHasOptionsMenu(true) 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) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_log, menu) super.onCreateOptionsMenu(menu, inflater)
menu.findItem(R.id.menu_save).isVisible = viewModel.currentPage.value == 1 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_save -> activity.withExternalRW { R.id.action_save -> viewModel.saveMagiskLog()
onSuccess { R.id.action_clear ->
viewModel.saveLog() if (isMagiskLogVisible) viewModel.clearMagiskLog()
} else viewModel.clearLog()
} }
R.id.menu_clear -> viewModel.clearLog() return super.onOptionsItemSelected(item)
R.id.menu_refresh -> viewModel.refresh()
} }
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 package com.topjohnwu.magisk.ui.log
import android.content.res.Resources
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R 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.data.repository.LogRepository
import com.topjohnwu.magisk.databinding.ComparableRvItem 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.extensions.subscribeK
import com.topjohnwu.magisk.model.binding.BindingAdapter import com.topjohnwu.magisk.model.binding.BindingAdapter
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem import com.topjohnwu.magisk.model.entity.recycler.ConsoleItem
import com.topjohnwu.magisk.model.entity.recycler.LogItemRvItem import com.topjohnwu.magisk.model.entity.recycler.LogItem
import com.topjohnwu.magisk.model.entity.recycler.LogRvItem import com.topjohnwu.magisk.model.entity.recycler.TextItem
import com.topjohnwu.magisk.model.entity.recycler.MagiskLogRvItem
import com.topjohnwu.magisk.model.events.PageChangedEvent
import com.topjohnwu.magisk.model.events.SnackbarEvent import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.utils.DiffObservableList import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.utils.KObservableField import com.topjohnwu.magisk.ui.base.diffListOf
import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter import io.reactivex.Completable
import me.tatarka.bindingcollectionadapter2.OnItemBind import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.util.* import java.util.*
class LogViewModel( class LogViewModel(
private val resources: Resources, private val repo: LogRepository
private val logRepo: LogRepository ) : BaseViewModel() {
) : 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 // --- empty view
private val magiskLogItem get() = items[1] as MagiskLogRvItem
val scrollPosition = KObservableField(0) val itemEmpty = TextItem(R.string.log_data_none)
val itemMagiskEmpty = TextItem(R.string.log_data_magisk_none)
init { // --- main view
currentPage.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
PageChangedEvent().publish()
}
items.addAll(listOf(LogRvItem(), MagiskLogRvItem())) val items = diffListOf<LogItem>()
refresh() val itemBinding = itemBindingOf<LogItem> {
it.bindExtra(BR.viewModel, this)
} }
override fun getPageTitle(position: Int, item: ComparableRvItem<*>?) = when (item) { // --- console
is LogRvItem -> resources.getString(R.string.superuser)
is MagiskLogRvItem -> resources.getString(R.string.magisk) val consoleAdapter = BindingAdapter()
else -> "" val itemsConsole = diffListOf<ComparableRvItem<*>>()
} val itemConsoleBinding = itemBindingOf<ComparableRvItem<*>> {}
fun scrollDownPressed() { override fun refresh(): Disposable {
scrollPosition.value = magiskLogItem.items.size - 1 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()
fun refresh() { return Completable.merge(listOf(logs, console)).subscribeK()
fetchLogs().subscribeK { logItem.update(it) }
fetchMagiskLog().subscribeK { magiskLogItem.update(it) }
} }
fun saveLog() { fun saveMagiskLog() {
val now = Calendar.getInstance() val now = Calendar.getInstance()
val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format( val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format(
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1, now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
...@@ -92,29 +96,18 @@ class LogViewModel( ...@@ -92,29 +96,18 @@ class LogViewModel(
} }
} }
fun clearLog() = when (currentItem) { fun clearMagiskLog() = repo.clearMagiskLogs()
is LogRvItem -> clearLogs { refresh() } .subscribeK {
is MagiskLogRvItem -> clearMagiskLogs { refresh() } SnackbarEvent(R.string.logs_cleared).publish()
else -> Unit requestRefresh()
} }
private fun clearLogs(callback: () -> Unit) = logRepo.clearLogs()
.doOnSubscribeUi(callback)
.subscribeK { SnackbarEvent(R.string.logs_cleared).publish() }
.add() .add()
private fun clearMagiskLogs(callback: () -> Unit) = logRepo.clearMagiskLogs() fun clearLog() = repo.clearLogs()
.doOnComplete(callback) .subscribeK {
.subscribeK { SnackbarEvent(R.string.logs_cleared).publish() } SnackbarEvent(R.string.logs_cleared).publish()
requestRefresh()
}
.add() .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 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.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.data.database.RepoDao 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.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.* import com.topjohnwu.magisk.extensions.reboot
import com.topjohnwu.magisk.model.entity.module.Module import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem import com.topjohnwu.magisk.model.entity.recycler.*
import com.topjohnwu.magisk.model.entity.recycler.SectionRvItem import com.topjohnwu.magisk.model.events.InstallExternalModuleEvent
import com.topjohnwu.magisk.model.events.InstallModuleEvent
import com.topjohnwu.magisk.model.events.OpenChangelogEvent import com.topjohnwu.magisk.model.events.OpenChangelogEvent
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent import com.topjohnwu.magisk.model.events.dialog.ModuleInstallDialog
import com.topjohnwu.magisk.tasks.RepoUpdater import com.topjohnwu.magisk.ui.base.*
import com.topjohnwu.magisk.utils.DiffObservableList import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
import com.topjohnwu.magisk.utils.KObservableField import com.topjohnwu.magisk.utils.KObservableField
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable 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( class ModuleViewModel(
private val resources: Resources, private val repoName: RepoByNameDao,
private val repoUpdater: RepoUpdater, private val repoUpdated: RepoByUpdatedDao,
private val repoDB: RepoDao private val repoUpdater: RepoUpdater
) : BaseViewModel() { ) : 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)
val query = KObservableField("") private val itemsInstalledHelpers = ObservableArrayList<TextItem>().also {
it.add(itemNoneInstalled)
}
private val itemsUpdatableHelpers = ObservableArrayList<TextItem>().also {
it.add(itemNoneUpdatable)
}
private val allItems = mutableListOf<ComparableRvItem<*>>() private val itemsCoreOnly = ObservableArrayList<SafeModeNotice>()
private val itemsInstalled = diffListOf<ModuleItem>()
private val itemsUpdatable = diffListOf<RepoItem.Update>()
private val itemsRemote = diffListOf<RepoItem.Remote>()
val itemsInstalled = DiffObservableList(ComparableRvItem.callback) val adapter = adapterOf<ComparableRvItem<*>>()
val itemsRemote = DiffObservableList(ComparableRvItem.callback) val items = MergeObservableList<ComparableRvItem<*>>()
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item -> .insertList(itemsCoreOnly)
item.bind(itemBinding) .insertItem(sectionActive)
itemBinding.bindExtra(BR.viewModel, this@ModuleViewModel) .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)
} }
private var queryDisposable: Disposable? = null 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 }
private val sectionActive = SectionTitle(
R.string.module_section_installed,
R.string.reboot,
R.drawable.ic_restart
).also { it.hasButton = false }
init { init {
query.addOnPropertyChangedCallback { updateOrderIcon()
queryDisposable?.dispose() }
queryDisposable = query()
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
} }
refresh(false) }
}
// ---
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()
} }
fun fabPressed() = OpenFilePickerEvent().publish() // ---
fun repoPressed(item: RepoRvItem) = OpenChangelogEvent(item.item).publish()
fun downloadPressed(item: RepoRvItem) = InstallModuleEvent(item.item).publish()
fun refresh(force: Boolean) { init {
Single.fromCallable { Module.loadModules() } RemoteFileService.reset()
.flattenAsFlowable { it } RemoteFileService.progressBroadcast.observeForever {
.map { ModuleRvItem(it) } val (progress, subject) = it ?: return@observeForever
.toList() 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) } .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) .applyViewModel(this)
.subscribeK { itemsRemote.update(it.first, it.second) } .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)
} }
.ignoreElement()!!
private fun query() = queryRaw() @Synchronized
.subscribeK { itemsRemote.update(it.first, it.second) } fun loadRemote() {
// check for existing jobs
if (remoteJob?.isDisposed?.not() == true) {
return
}
if (itemsRemote.isEmpty()) {
EndlessRecyclerScrollListener.ResetState().publish()
}
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
}
private fun queryRaw(query: String = this.query.value) = allItems.toSingle() fun forceRefresh() {
.map { it.filterIsInstance<RepoRvItem>() } itemsRemote.clear()
.flattenAsFlowable { it } itemsSearch.clear()
.filter { refetch = true
it.item.name.contains(query, ignoreCase = true) || refresh()
it.item.author.contains(query, ignoreCase = true) || submitQuery()
it.item.description.contains(query, ignoreCase = true)
} }
.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 <T : ComparableRvItem<*>> List<T>.withTitle(text: Int) = override fun submitQuery() {
if (isEmpty()) this else listOf(SectionRvItem(resources.getString(text))) + this queryHandler.removeCallbacks(queryRunnable)
queryHandler.postDelayed(queryRunnable, queryDelay)
}
val groupedItems = groupBy { repo -> private fun queryInternal(query: String, offset: Int): Single<List<RepoItem>> {
installed.firstOrNull { it.item.id == repo.item.id }?.let { if (query.isBlank()) {
if (it.item.versionCode < repo.item.versionCode) MODULE_UPDATABLE return Single.just(listOf<RepoItem>())
else MODULE_INSTALLED .doOnSubscribe { itemsSearch.clear() }
} ?: MODULE_REMOTE .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) + private fun query(query: String = this.query, offset: Int = 0) {
groupedItems.getOrElse(MODULE_INSTALLED) { listOf() }.withTitle(R.string.installed) + queryJob?.dispose()
groupedItems.getOrElse(MODULE_REMOTE) { listOf() }.withTitle(R.string.not_installed) 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 { @Synchronized
protected const val MODULE_INSTALLED = 0 fun loadMoreQuery() {
protected const val MODULE_REMOTE = 1 if (queryJob?.isDisposed == false) return
protected const val MODULE_UPDATABLE = 2 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()
} }
} }
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 package com.topjohnwu.magisk.ui.settings
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.view.View
import android.view.LayoutInflater import androidx.core.graphics.Insets
import android.widget.EditText import com.topjohnwu.magisk.R
import android.widget.Toast import com.topjohnwu.magisk.databinding.FragmentSettingsMd2Binding
import androidx.appcompat.app.AlertDialog import com.topjohnwu.magisk.ui.base.CompatFragment
import androidx.databinding.DataBindingUtil import com.topjohnwu.magisk.utils.PinchZoomTouchListener
import androidx.preference.ListPreference import org.koin.androidx.viewmodel.ext.android.viewModel
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
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 override fun consumeSystemWindowInsets(insets: Insets) = insets
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 onStart() { override fun onStart() {
super.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 activity.title = resources.getString(R.string.section_settings)
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) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (activity.packageName == BuildConfig.APPLICATION_ID) { super.onViewCreated(view, savedInstanceState)
generalCategory.removePreference(restoreManager) PinchZoomTouchListener.attachTo(binding.settingsList)
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()) { override fun onDestroyView() {
preferenceScreen.removePreference(suCategory) PinchZoomTouchListener.clear(binding.settingsList)
super.onDestroyView()
} }
if (!Info.env.isActive) { override fun onResume() {
preferenceScreen.removePreference(magiskCategory) super.onResume()
generalCategory.removePreference(hideManager) viewModel.items.forEach { it.refresh() }
}
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()
}
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 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
}
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)
}
}
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 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.R
import com.topjohnwu.magisk.base.BaseFragment import com.topjohnwu.magisk.databinding.FragmentSuperuserMd2Binding
import com.topjohnwu.magisk.databinding.FragmentSuperuserBinding 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 import org.koin.androidx.viewmodel.ext.android.viewModel
class SuperuserFragment : class SuperuserFragment : CompatFragment<SuperuserViewModel, FragmentSuperuserMd2Binding>() {
BaseFragment<SuperuserViewModel, FragmentSuperuserBinding>() {
override val layoutRes: Int = R.layout.fragment_superuser override val layoutRes = R.layout.fragment_superuser_md2
override val viewModel: SuperuserViewModel by viewModel() override val viewModel by viewModel<SuperuserViewModel>()
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
activity.title = resources.getString(R.string.section_superuser)
setHasOptionsMenu(true) setHasOptionsMenu(true)
requireActivity().setTitle(R.string.superuser)
} }
override fun onResume() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onResume() super.onViewCreated(view, savedInstanceState)
viewModel.updatePolicies() 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 ...@@ -2,150 +2,162 @@ package com.topjohnwu.magisk.ui.superuser
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import androidx.databinding.ObservableArrayList
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.data.database.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.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.applySchedulers import com.topjohnwu.magisk.extensions.applySchedulers
import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.toggle import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskPolicy import com.topjohnwu.magisk.model.entity.recycler.PolicyItem
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem import com.topjohnwu.magisk.model.entity.recycler.TappableHeadlineItem
import com.topjohnwu.magisk.model.events.PolicyEnableEvent import com.topjohnwu.magisk.model.entity.recycler.TextItem
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
import com.topjohnwu.magisk.model.events.SnackbarEvent import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.utils.BiometricHelper import com.topjohnwu.magisk.model.events.dialog.BiometricDialog
import com.topjohnwu.magisk.utils.DiffObservableList import com.topjohnwu.magisk.model.events.dialog.SuperuserRevokeDialog
import com.topjohnwu.magisk.utils.RxBus import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog 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.Single
import io.reactivex.disposables.Disposable import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
import me.tatarka.bindingcollectionadapter2.ItemBinding
class SuperuserViewModel( class SuperuserViewModel(
private val policyDB: PolicyDao, private val db: PolicyDao,
private val packageManager: PackageManager, private val packageManager: PackageManager,
private val resources: Resources, private val resources: Resources
rxBus: RxBus ) : BaseViewModel(), TappableHeadlineItem.Listener {
) : BaseViewModel() {
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@SuperuserViewModel)
}
private var ignoreNext: PolicyRvItem? = null private val itemNoData = TextItem(R.string.superuser_policy_none)
private var fetchTask: Disposable? = null
init { private val itemsPolicies = diffListOf<PolicyItem>()
rxBus.register<PolicyEnableEvent>() private val itemsHelpers = ObservableArrayList<TextItem>().also {
.filter { it.add(itemNoData)
val isIgnored = it.item == ignoreNext
if (isIgnored) ignoreNext = null
!isIgnored
} }
.subscribeK { togglePolicy(it.item, it.enable) }
.add()
rxBus.register<PolicyUpdateEvent>()
.subscribeK { updatePolicy(it) }
.add()
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() override fun refresh() = db.fetchAll()
.flattenAsFlowable { it } .flattenAsFlowable { it }
.map { PolicyRvItem(it, it.applicationInfo.loadIcon(packageManager)) } .parallel()
.toList() .map { PolicyItem(it, it.applicationInfo.loadIcon(packageManager)) }
.map { .sequential()
it.sortedWith(compareBy( .sorted { o1, o2 ->
{ it.item.appName.toLowerCase() }, compareBy<PolicyItem>(
{ it.item.appName.toLowerCase(currentLocale) },
{ it.item.packageName } { it.item.packageName }
)) ).compare(o1, o2)
} }
.map { it to items.calculateDiff(it) } .toList()
.map { it to itemsPolicies.calculateDiff(it) }
.applySchedulers() .applySchedulers()
.applyViewModel(this) .applyViewModel(this)
.subscribeK { items.update(it.first, it.second) } .subscribeK {
itemsPolicies.update(it.first, it.second)
if (itemsPolicies.isNotEmpty()) {
itemsHelpers.remove(itemNoData)
}
}
// ---
@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) fun updateState() = deletePolicy(item.item)
.map { items.filterIsInstance<PolicyRvItem>().toMutableList() } .subscribeK {
.map { it.removeAll { it.item.packageName == item.item.packageName }; it } itemsPolicies.removeAll { it.genericItemSameAs(item) }
.map { it to items.calculateDiff(it) } if (itemsPolicies.isEmpty() && itemsHelpers.isEmpty()) {
.subscribeK { items.update(it.first, it.second) } itemsHelpers.add(itemNoData)
}
}
.add() .add()
withView {
if (BiometricHelper.isEnabled) { if (BiometricHelper.isEnabled) {
BiometricHelper.authenticate(this) { updateState() } BiometricDialog {
onSuccess { updateState() }
}.publish()
} else { } else {
CustomAlertDialog(this) SuperuserRevokeDialog {
.setTitle(R.string.su_revoke_title) appName = item.item.appName
.setMessage(getString(R.string.su_revoke_msg, item.item.appName)) onSuccess { updateState() }
.setPositiveButton(android.R.string.yes) { _, _ -> updateState() } }.publish()
.setNegativeButton(android.R.string.no, null)
.setCancelable(true)
.show()
}
} }
} }
private fun updatePolicy(it: PolicyUpdateEvent) = when (it) { //---
is PolicyUpdateEvent.Notification -> updatePolicy(it.item) {
val textId = fun updatePolicy(it: PolicyUpdateEvent) = when (it) {
if (it.notification) R.string.su_snack_notif_on else R.string.su_snack_notif_off is PolicyUpdateEvent.Notification -> updatePolicy(it.item).map {
val text = resources.getString(textId).format(it.appName) when {
SnackbarEvent(text).publish() it.notification -> R.string.su_snack_notif_on
} else -> R.string.su_snack_notif_off
is PolicyUpdateEvent.Log -> updatePolicy(it.item) { } to it.appName
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) }
private fun updatePolicy(item: MagiskPolicy, onSuccess: (MagiskPolicy) -> Unit) = .subscribeK { SnackbarEvent(it).publish() }
updatePolicy(item)
.subscribeK { onSuccess(it) }
.add() .add()
private fun togglePolicy(item: PolicyRvItem, enable: Boolean) { fun togglePolicy(item: PolicyItem, enable: Boolean) {
fun updateState() { 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) updatePolicy(app)
.map { it.policy == MagiskPolicy.ALLOW } .map { it.policy == MagiskPolicy.ALLOW }
.subscribeK { .map { if (it) R.string.su_snack_grant else R.string.su_snack_deny }
val textId = if (it) R.string.su_snack_grant else R.string.su_snack_deny .map { resources.getString(it).format(item.item.appName) }
val text = resources.getString(textId).format(item.item.appName) .subscribeK { SnackbarEvent(it).publish() }
SnackbarEvent(text).publish()
}
.add() .add()
} }
if (BiometricHelper.isEnabled) { if (BiometricHelper.isEnabled) {
withView { BiometricDialog {
BiometricHelper.authenticate(this, onError = { onSuccess { updateState() }
ignoreNext = item onFailure { item.isEnabled.toggle() }
item.isEnabled.toggle() }.publish()
}) { updateState() }
}
} else { } else {
updateState() updateState()
} }
} }
//---
private fun updatePolicy(policy: MagiskPolicy) = private fun updatePolicy(policy: MagiskPolicy) =
policyDB.update(policy).andThen(Single.just(policy)) db.update(policy).andThen(Single.just(policy))
private fun deletePolicy(policy: MagiskPolicy) = 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 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.View
import android.view.ViewAnimationUtils
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextSwitcher
import android.widget.TextView import android.widget.TextView
import android.widget.ViewSwitcher
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.animation.doOnEnd
import androidx.core.view.*
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener import androidx.databinding.InverseBindingListener
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager 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.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.extensions.drawableCompat
import com.topjohnwu.magisk.extensions.replaceRandomWithSpecial import com.topjohnwu.magisk.extensions.replaceRandomWithSpecial
import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.state.IndeterminateState import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.hypot
import kotlin.math.roundToInt
@BindingAdapter("onNavigationClick") @BindingAdapter("onNavigationClick")
...@@ -229,3 +248,236 @@ fun TextInputLayout.setErrorString(error: String) { ...@@ -229,3 +248,236 @@ fun TextInputLayout.setErrorString(error: String) {
if (this.error == null && newError == null) return if (this.error == null && newError == null) return
this.error = newError 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>( ...@@ -18,9 +18,9 @@ open class DiffObservableList<T>(
) : AbstractList<T>(), ObservableList<T> { ) : AbstractList<T>(), ObservableList<T> {
private val LIST_LOCK = Object() private val LIST_LOCK = Object()
private var list: MutableList<T> = ArrayList() protected var list: MutableList<T> = ArrayList()
private val listeners = ListChangeRegistry() private val listeners = ListChangeRegistry()
private val listCallback = ObservableListUpdateCallback() protected val listCallback = ObservableListUpdateCallback()
override val size: Int get() = list.size override val size: Int get() = list.size
...@@ -38,7 +38,7 @@ open class DiffObservableList<T>( ...@@ -38,7 +38,7 @@ open class DiffObservableList<T>(
return doCalculateDiff(frozenList, newItems) 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() { return DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize() = oldItems.size 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 ...@@ -10,6 +10,7 @@ import androidx.core.view.get
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.extensions.drawableCompat import com.topjohnwu.magisk.extensions.drawableCompat
import kotlin.math.roundToInt
class KItemDecoration( class KItemDecoration(
private val context: Context, private val context: Context,
...@@ -63,7 +64,7 @@ class KItemDecoration( ...@@ -63,7 +64,7 @@ class KItemDecoration(
.map { parent[it] } .map { parent[it] }
.forEach { child -> .forEach { child ->
parent.getDecoratedBoundsWithMargins(child, bounds) parent.getDecoratedBoundsWithMargins(child, bounds)
val bottom = bounds.bottom + Math.round(child.translationY) val bottom = bounds.bottom + child.translationY.roundToInt()
val top = bottom - drawable.intrinsicHeight val top = bottom - drawable.intrinsicHeight
drawable.setBounds(left, top, right, bottom) drawable.setBounds(left, top, right, bottom)
drawable.draw(canvas) drawable.draw(canvas)
...@@ -93,7 +94,7 @@ class KItemDecoration( ...@@ -93,7 +94,7 @@ class KItemDecoration(
.map { parent[it] } .map { parent[it] }
.forEach { child -> .forEach { child ->
parent.layoutManager!!.getDecoratedBoundsWithMargins(child, bounds) 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 val left = right - drawable.intrinsicWidth
drawable.setBounds(left, top, right, bottom) drawable.setBounds(left, top, right, bottom)
drawable.draw(canvas) drawable.draw(canvas)
......
...@@ -2,18 +2,30 @@ package com.topjohnwu.magisk.utils ...@@ -2,18 +2,30 @@ package com.topjohnwu.magisk.utils
import androidx.databinding.Observable import androidx.databinding.Observable
import androidx.databinding.ObservableField import androidx.databinding.ObservableField
import com.topjohnwu.magisk.model.observer.Observer
import java.io.Serializable import java.io.Serializable
/** /**
* Kotlin version of [ObservableField]. * Kotlin version of [ObservableField].
* You can define if wrapped type is Nullable or not. * You can define if wrapped type is Nullable or not.
* You can use kotlin get/set syntax for value * 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 var value: T
get() = get() set(value) {
set(value) { set(value) } if (field != value) {
field = value
notifyChange()
}
}
constructor(init: T) { constructor(init: T) {
value = init value = init
...@@ -23,8 +35,23 @@ open class KObservableField<T> : ObservableField<T>, Serializable { ...@@ -23,8 +35,23 @@ open class KObservableField<T> : ObservableField<T>, Serializable {
value = init value = init
} }
@Suppress("UNCHECKED_CAST") @Deprecated(
message = "Needed for data binding, use KObservableField.value syntax from code",
replaceWith = ReplaceWith("value")
)
override fun get(): T { 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 ...@@ -3,7 +3,6 @@ package com.topjohnwu.magisk.view
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.repository.StringRepository import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.extensions.subscribeK
...@@ -34,7 +33,8 @@ object MarkDownWindow : KoinComponent { ...@@ -34,7 +33,8 @@ object MarkDownWindow : KoinComponent {
} }
fun show(activity: Context, title: String?, content: Single<String>) { 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) val tv = mv.findViewById<TextView>(R.id.md_txt)
content.map { content.map {
...@@ -45,11 +45,14 @@ object MarkDownWindow : KoinComponent { ...@@ -45,11 +45,14 @@ object MarkDownWindow : KoinComponent {
tv.setText(R.string.download_file_error) tv.setText(R.string.download_file_error)
Completable.complete() Completable.complete()
}.subscribeK { }.subscribeK {
AlertDialog.Builder(activity) MagiskDialog(activity)
.setTitle(title) .applyTitle(title ?: "")
.setView(mv) .applyView(mv)
.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() } .applyButton(MagiskDialog.ButtonType.NEGATIVE) {
.show() 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"?> <?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item> <item>
<shape android:shape="oval"> <shape android:shape="rectangle">
<solid android:color="@color/su_request_background"/> <solid android:color="?colorSurfaceVariant" />
</shape> </shape>
</item> </item>
<item>
<inset <item android:bottom="2dp">
android:drawable="@drawable/ic_cloud_download" <shape android:shape="rectangle">
android:inset="13dp"/> <solid android:color="?colorSurface" />
</shape>
</item> </item>
</layer-list> </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" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24"
android:viewportWidth="24.0"> android:viewportHeight="24">
<path <path
android:fillColor="#FF000000" 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"/> 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> </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"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24" android:viewportWidth="24"
android:width="24dp"> android:viewportHeight="24">
<path <path
android:fillColor="#000000" android:fillColor="?colorOnSurface"
android:pathData="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" /> 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" />
<path
android:pathData="M0-0.75h24v24H0z" />
</vector> </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" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp" android:height="24dp"
android:width="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="#fff" android:fillColor="?colorOnSurface"
android:pathData="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" /> android:pathData="M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1Z" />
</vector> </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 @@ ...@@ -5,6 +5,6 @@
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="#000" android:fillColor="?colorOnSurface"
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: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> </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 @@ ...@@ -7,7 +7,7 @@
<variable <variable
name="viewModel" name="viewModel"
type="com.topjohnwu.magisk.ui.flash.FlashViewModel" /> type="com.topjohnwu.magisk.legacy.flash.FlashViewModel" />
</data> </data>
......
<?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 @@ ...@@ -7,7 +7,7 @@
<variable <variable
name="viewModel" name="viewModel"
type="com.topjohnwu.magisk.ui.surequest.SuRequestViewModel" /> type="com.topjohnwu.magisk.legacy.surequest.SuRequestViewModel" />
</data> </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 @@ ...@@ -5,42 +5,48 @@
<data> <data>
<import type="com.topjohnwu.magisk.extensions.XStringKt" />
<variable <variable
name="data" name="data"
type="com.topjohnwu.magisk.ui.settings.SettingsFragment.ManagerNameData" /> type="com.topjohnwu.magisk.ui.settings.Hide" />
</data> </data>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:paddingStart="@dimen/margin_generic" android:padding="@dimen/margin_generic">
android:paddingTop="@dimen/margin_generic"
android:paddingEnd="@dimen/margin_generic">
<com.google.android.material.textfield.TextInputLayout <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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic"
android:hint="@string/settings_app_name_hint" android:hint="@string/settings_app_name_hint"
app:boxStrokeColor="?colorOnSurfaceVariant"
app:counterEnabled="true" app:counterEnabled="true"
app:counterMaxLength="14" app:counterMaxLength="14"
app:counterOverflowTextColor="@color/colorError" 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: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 <com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_custom_download_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@={data.name}" android:inputType="textCapWords"
tools:text="@string/re_app_name" /> android:text="@={data.value}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textColor="?colorOnSurface"
tools:text="@tools:sample/lorem" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
</LinearLayout> </LinearLayout>
</layout> </layout>
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<variable <variable
name="data" name="data"
type="com.topjohnwu.magisk.ui.settings.SettingsFragment.DownloadDialogData" /> type="com.topjohnwu.magisk.ui.settings.DownloadPath" />
</data> </data>
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@{@string/settings_download_path_message(data.path)}" android:text="@{@string/settings_download_path_message(data.path)}"
android:textAppearance="@style/AppearanceFoundation.Caption"
tools:text="@string/settings_download_path_message" /> tools:text="@string/settings_download_path_message" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
...@@ -30,14 +31,21 @@ ...@@ -30,14 +31,21 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic" android:layout_marginTop="@dimen/margin_generic"
android:hint="@string/settings_download_path_title" 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 <com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_custom_download_text" android:id="@+id/dialog_custom_download_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="textUri" 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> </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" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<data> <data>
<import type="com.topjohnwu.magisk.R" />
<variable <variable
name="viewModel" name="viewModel"
type="com.topjohnwu.magisk.ui.superuser.SuperuserViewModel" /> type="com.topjohnwu.magisk.ui.superuser.SuperuserViewModel" />
...@@ -15,39 +18,48 @@ ...@@ -15,39 +18,48 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/superuser_content" android:id="@+id/superuser_list"
dividerColor="@{@android:color/transparent}" adapter="@{viewModel.adapter}"
dividerSize="@{@dimen/margin_generic}" dividerHorizontal="@{R.drawable.divider_l1}"
gone="@{!viewModel.loaded}" dividerVertical="@{R.drawable.divider_l1}"
goneUnless="@{viewModel.loaded || !viewModel.items.empty}"
itemBinding="@{viewModel.itemBinding}" itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}" items="@{viewModel.items}"
nestedScrollingEnabled="@{false}"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/margin_generic" android:paddingStart="@dimen/l1"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
tools:listitem="@layout/item_policy" /> android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l2}"
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
<ProgressBar app:spanCount="2"
style="@style/Widget.Progress" tools:layout_marginTop="24dp"
gone="@{!viewModel.loading}" tools:listitem="@layout/item_policy_md2"
android:layout_gravity="center" /> tools:paddingTop="@dimen/l1" />
<androidx.appcompat.widget.AppCompatTextView <LinearLayout
android:id="@+id/superuser_empty" goneUnless="@{viewModel.loading &amp;&amp; viewModel.items.empty}"
gone="@{!viewModel.loaded || viewModel.items.size &gt; 0}"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:fontFamily="sans-serif-light"
android:gravity="center" android:gravity="center"
android:text="@string/no_apps_found" android:orientation="vertical">
android:textSize="20sp"
android:textStyle="italic" />
</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 @@ ...@@ -5,10 +5,6 @@
<data> <data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.MagiskLogRvItem" />
<variable <variable
name="viewModel" name="viewModel"
type="com.topjohnwu.magisk.ui.log.LogViewModel" /> type="com.topjohnwu.magisk.ui.log.LogViewModel" />
...@@ -24,33 +20,35 @@ ...@@ -24,33 +20,35 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
adapter="@{viewModel.itemsAdapter}" adapter="@{viewModel.consoleAdapter}"
itemBinding="@{viewModel.itemBinding}" itemBinding="@{viewModel.itemConsoleBinding}"
items="@{item.items}" items="@{viewModel.itemsConsole}"
scrollPosition="@={viewModel.scrollPosition}"
scrollPositionSmooth="@{true}"
android:focusable="true"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical" 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" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_console" /> tools:listitem="@layout/item_console"
tools:paddingTop="24dp" />
</HorizontalScrollView> </HorizontalScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton <FrameLayout
hide="@{viewModel.scrollPosition == item.items.size - 1 || item.items.size == 0}" gone="@{!viewModel.itemsConsole.empty}"
android:focusable="true" android:layout_width="match_parent"
android:clickable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="center">
android:layout_margin="@dimen/margin_generic"
android:onClick="@{() -> viewModel.scrollDownPressed()}" <include
app:srcCompat="@drawable/ic_arrow_down" item="@{viewModel.itemMagiskEmpty}"
app:tint="@color/colorTextInverse" /> layout="@layout/item_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout> </FrameLayout>
</FrameLayout>
</layout> </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 @@ ...@@ -7,7 +7,7 @@
<variable <variable
name="item" name="item"
type="com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem" /> type="com.topjohnwu.magisk.model.entity.recycler.HideProcessItem" />
<variable <variable
name="viewModel" name="viewModel"
...@@ -17,40 +17,41 @@ ...@@ -17,40 +17,41 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
android:clickable="false" android:onClick="@{() -> item.toggle(viewModel)}"
android:focusable="false" android:layout_height="wrap_content"
android:onClick="@{() -> item.toggle()}"> android:layout_gravity="center">
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_generic" android:layout_marginStart="@dimen/l1"
android:ellipsize="marquee" android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
android:singleLine="true" android:singleLine="true"
android:text="@{item.process}" android:text="@{item.item.name}"
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_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_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="com.topjohnwu.magisk.process" /> tools:text="com.topjohnwu.magisk" />
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_process_icon" android:id="@+id/hide_process_checkbox"
style="@style/Widget.Icon" style="@style/WidgetFoundation.Image.Small"
isChecked="@{item.isHidden}" android:layout_width="18dp"
android:background="@null" android:layout_height="18dp"
android:clickable="false" isSelected="@{item.isHidden}"
android:focusable="false" android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/imageColorTint" app:srcCompat="@drawable/ic_radio_check_button"
tools:src="@drawable/ic_checked" /> app:tint="?colorPrimary" />
</androidx.constraintlayout.widget.ConstraintLayout> </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 @@ ...@@ -7,7 +7,7 @@
<variable <variable
name="item" name="item"
type="com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem" /> type="com.topjohnwu.magisk.model.entity.recycler.PolicyItem" />
<variable <variable
name="viewModel" name="viewModel"
...@@ -16,191 +16,163 @@ ...@@ -16,191 +16,163 @@
</data> </data>
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
style="@style/Widget.Card" style="@style/WidgetFoundation.Card.Variant"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:foreground="?attr/selectableItemBackground" android:alpha="@{item.isEnabled() ? 1f : .5f}"
android:minHeight="?android:attr/listPreferredItemHeight" android:onClick="@{() -> item.toggle(viewModel)}"
android:onClick="@{() -> item.toggle()}" android:onLongClick="@{() -> item.toggle()}"
app:cardCornerRadius="@dimen/radius_generic" tools:layout_marginBottom="@dimen/l1"
app:cardElevation="2dp" tools:layout_marginEnd="@dimen/l1"
app:cardPreventCornerOverlap="true"> tools:layout_width="200dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical"> android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content">
android:padding="@dimen/margin_generic_half">
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/app_icon" android:id="@+id/policy_app_icon"
android:layout_width="50dp" style="@style/WidgetFoundation.Image"
android:layout_height="50dp" android:layout_marginStart="@dimen/l1"
android:gravity="end" android:layout_marginTop="@dimen/l1"
android:layout_marginBottom="@dimen/l1"
android:src="@{item.icon}" android:src="@{item.icon}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/app_name"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
tools:srcCompat="@drawable/ic_logo" /> 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 <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/app_name" android:id="@+id/policy_app_name"
strikeThrough="@{!item.enabled}"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_generic" android:layout_marginLeft="@dimen/margin_generic"
android:layout_marginRight="@dimen/margin_generic" android:layout_marginRight="@dimen/margin_generic"
android:ellipsize="end" android:ellipsize="middle"
android:maxLines="1" android:gravity="start"
android:maxLines="2"
android:text="@{item.item.appName}" android:text="@{item.item.appName}"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="@style/AppearanceFoundation.Body"
android:textIsSelectable="false" android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/package_name" android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/master_switch" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/app_icon" app:layout_constraintStart_toEndOf="@+id/policy_app_icon"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="@+id/policy_app_icon"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/app_name" /> tools:text="@string/app_name" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/package_name" android:id="@+id/policy_package_name"
strikeThrough="@{!item.enabled}"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:layout_marginBottom="@dimen/l1"
android:maxLines="1" android:ellipsize="middle"
android:gravity="start"
android:maxLines="2"
android:text="@{item.item.packageName}" android:text="@{item.item.packageName}"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
android:textColor="@android:color/tertiary_text_dark" android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false" android:textIsSelectable="false"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/app_name" app:layout_constraintEnd_toEndOf="@id/policy_app_name"
app:layout_constraintStart_toStartOf="@id/app_name" app:layout_constraintStart_toStartOf="@id/policy_app_name"
app:layout_constraintTop_toBottomOf="@id/app_name" app:layout_constraintTop_toBottomOf="@id/policy_app_name"
app:layout_constraintVertical_bias="0"
tools:text="com.topjohnwu.magisk" /> 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>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/expand_layout" android:id="@+id/policy_expand_container"
gone="@{!item.isExpanded}" gone="@{!item.isExpanded}"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:paddingBottom="@dimen/margin_generic_half" android:background="?colorSurfaceVariant"
android:paddingTop="@dimen/margin_generic_half" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/package_name"> app:layout_constraintTop_toBottomOf="@+id/policy_package_name"
tools:visibility="visible">
<androidx.appcompat.widget.AppCompatImageView <com.google.android.material.button.MaterialButton
android:id="@+id/bell" android:id="@+id/policy_notify"
android:layout_width="wrap_content" style="@style/WidgetFoundation.Button.Text"
isSelected="@{item.shouldNotify}"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:minHeight="24dp"
android:layout_marginStart="16dp" android:onClick="@{() -> item.toggleNotify(viewModel)}"
android:tint="@color/icon_grey" android:text="@string/superuser_toggle_notification"
app:layout_constraintBottom_toBottomOf="parent" android:textAllCaps="false"
app:layout_constraintEnd_toStartOf="@+id/notification_switch" 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_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_notifications" /> app:tint="@color/color_state_primary_transient" />
<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" />
<androidx.appcompat.widget.SwitchCompat <com.google.android.material.button.MaterialButton
android:id="@+id/logging_switch" android:id="@+id/policy_log"
android:layout_width="wrap_content" style="@style/WidgetFoundation.Button.Text"
android:layout_height="wrap_content" isSelected="@{item.shouldLog}"
android:layout_gravity="center_vertical" android:layout_width="match_parent"
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:layout_height="wrap_content"
android:orientation="vertical" android:minHeight="24dp"
app:layout_constraintGuide_percent="0.8" /> 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 <com.google.android.material.button.MaterialButton
android:id="@+id/more_info" android:id="@+id/policy_delete"
android:layout_width="wrap_content" style="@style/WidgetFoundation.Button.Text"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:minHeight="24dp"
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:onClick="@{() -> viewModel.deletePressed(item)}" 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_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/more_info" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toBottomOf="@+id/policy_log"
app:srcCompat="@drawable/ic_delete" /> app:rippleColor="?colorError" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
......
...@@ -5,9 +5,11 @@ ...@@ -5,9 +5,11 @@
<data> <data>
<import type="com.topjohnwu.magisk.R" />
<variable <variable
name="item" name="item"
type="com.topjohnwu.magisk.model.entity.recycler.RepoRvItem" /> type="com.topjohnwu.magisk.model.entity.recycler.RepoItem" />
<variable <variable
name="viewModel" name="viewModel"
...@@ -16,114 +18,132 @@ ...@@ -16,114 +18,132 @@
</data> </data>
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
style="@style/Widget.Card" style="@style/WidgetFoundation.Card.Variant"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground" tools:layout_gravity="center"
android:minHeight="?android:attr/listPreferredItemHeight" tools:layout_marginBottom="@dimen/l1"
android:onClick="@{() -> viewModel.repoPressed(item)}" tools:layout_marginEnd="@dimen/l1">
app:cardElevation="2dp"
tools:layout_gravity="center">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:padding="@dimen/margin_generic">
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title" android:id="@+id/module_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" 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:text="@{item.item.name}"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="@style/AppearanceFoundation.Body"
android:textIsSelectable="false" android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/version_name" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/download"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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: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 <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/author" android:id="@+id/module_version_author"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@{item.item.author.length == 0 ? @string/no_info_provided : @string/author(item.item.author)}" android:text="@{@string/module_version_author(item.item.version ?? `?`, item.item.author ?? `?`)}"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
android:textColor="@android:color/tertiary_text_dark" app:layout_constraintEnd_toEndOf="@+id/module_title"
android:textIsSelectable="false" app:layout_constraintStart_toStartOf="@+id/module_title"
android:textStyle="bold|italic" app:layout_constraintTop_toBottomOf="@+id/module_title"
app:layout_constraintBottom_toTopOf="@+id/description" tools:text="v1 by topjohnwu" />
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 <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/description" android:id="@+id/module_description"
android:layout_width="0dp" gone="@{item.item.description.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_marginStart="@dimen/l1"
android:text="@{item.item.description.length == 0 ? @string/no_info_provided : item.item.description}" android:layout_marginTop="@dimen/l1"
android:textAppearance="?android:attr/textAppearanceSmall" android:layout_marginEnd="@dimen/l1"
android:textIsSelectable="false" android:text="@{item.item.description}"
app:layout_constraintBottom_toTopOf="@+id/update_time" android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintEnd_toEndOf="@id/title" app:layout_constraintTop_toBottomOf="@+id/module_version_author"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/author"
tools:lines="4" tools:lines="4"
tools:text="@tools:sample/lorem/random" /> 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 <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/update_time"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_marginStart="@dimen/l1"
android:text="@{@string/updated_on(item.item.lastUpdateString)}" android:text="@{item.item.lastUpdateString}"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
android:textColor="@android:color/tertiary_text_dark" android:textSize="11sp"
android:textStyle="bold|italic"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/title" app:layout_constraintEnd_toStartOf="@+id/module_info"
app:layout_constraintStart_toStartOf="@+id/title" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/description" app:layout_constraintTop_toBottomOf="@+id/module_divider"
tools:ignore="SmallSp"
tools:text="@tools:sample/date/ddmmyy" /> tools:text="@tools:sample/date/ddmmyy" />
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/download" android:id="@+id/module_info"
android:focusable="true" style="@style/WidgetFoundation.Icon"
android:clickable="true"
style="@style/Widget.Icon"
isEnabled="@{viewModel.isConnected}"
android:alpha="@{viewModel.isConnected ? 1f : .2f}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:alpha=".5"
android:layout_gravity="center_vertical" android:onClick="@{() -> viewModel.infoPressed(item)}"
android:layout_marginStart="8dp" android:paddingEnd="@dimen/l_50"
android:onClick="@{() -> viewModel.downloadPressed(item)}" app:layout_constraintBottom_toBottomOf="@+id/module_download"
android:tint="?attr/imageColorTint" 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_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/title" app:layout_constraintTop_toBottomOf="@+id/module_divider"
app:layout_constraintTop_toTopOf="parent" android:onClick="@{() -> viewModel.downloadPressed(item)}"
app:srcCompat="@drawable/ic_file_download_black" /> tools:srcCompat="@drawable/ic_download_md2" />
</androidx.constraintlayout.widget.ConstraintLayout> </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> </com.google.android.material.card.MaterialCardView>
</layout> </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 @@ ...@@ -9,6 +9,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="15dp" android:layout_marginStart="15dp"
android:layout_marginEnd="15dp" android:layout_marginEnd="15dp"
android:paddingTop="10dp" /> android:paddingTop="10dp"
android:textAppearance="@style/AppearanceFoundation.Caption" />
</ScrollView> </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 @@ ...@@ -3,14 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/app_search" android:id="@+id/action_settings"
android:title="" android:icon="@drawable/ic_settings_md2"
app:actionViewClass="android.widget.SearchView" android:title="@string/section_settings"
app:showAsAction="always"/> app:showAsAction="ifRoom" />
<item
android:id="@+id/show_system"
android:title="@string/show_system_app"
android:checkable="true"/>
</menu> </menu>
\ No newline at end of file
...@@ -3,22 +3,15 @@ ...@@ -3,22 +3,15 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/menu_save" android:id="@+id/action_save"
android:icon="@drawable/ic_save" android:icon="@drawable/ic_save_md2"
android:title="@string/menuSaveLog" android:title="@string/menuSaveLog"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/menu_clear" android:id="@+id/action_clear"
android:icon="@drawable/ic_delete" android:icon="@drawable/ic_delete_md2"
android:title="@string/menuClearLog" android:title="@string/menuClearLog"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_refresh"
android:icon="@drawable/ic_refresh"
android:title="@string/menuReload"
app:showAsAction="ifRoom"/>
</menu> </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 @@ ...@@ -3,15 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/repo_sort" android:id="@+id/action_log"
android:title="" android:icon="@drawable/ic_bug_md2"
android:icon="@drawable/ic_sort" android:title="@string/section_log"
app:showAsAction="always"/> app:showAsAction="ifRoom" />
<item
android:id="@+id/repo_search"
android:title=""
app:actionViewClass="android.widget.SearchView"
app:showAsAction="always"/>
</menu> </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 @@ ...@@ -91,4 +91,10 @@
<item>@string/sort_by_update</item> <item>@string/sort_by_update</item>
</string-array> </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> </resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<attr name="cardStyle" format="reference"/> <!--region Deprecated-->
<attr name="cardStyle" format="reference" />
<attr name="colorAccentFallback" format="reference" /> <attr name="colorAccentFallback" format="reference" />
<attr name="imageColorTint" 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> </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 @@ ...@@ -3,13 +3,22 @@
<dimen name="card_elevation">2dp</dimen> <dimen name="card_elevation">2dp</dimen>
<dimen name="fab_padding">16dp</dimen> <dimen name="fab_padding">16dp</dimen>
<!--new dimens-->
<dimen name="margin_generic_double">32dp</dimen> <dimen name="margin_generic_double">32dp</dimen>
<dimen name="margin_generic">16dp</dimen> <dimen name="margin_generic">16dp</dimen>
<dimen name="margin_generic_half">8dp</dimen> <dimen name="margin_generic_half">8dp</dimen>
<dimen name="radius_generic">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> </resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<item name="hideWhenSelected" type="id" />
<item name="recyclerScrollListener" type="id" /> <item name="recyclerScrollListener" type="id" />
<item name="revealAnim" type="id" />
</resources> </resources>
\ No newline at end of file
...@@ -137,9 +137,11 @@ ...@@ -137,9 +137,11 @@
<string name="settings_update_channel_title">Update Channel</string> <string name="settings_update_channel_title">Update Channel</string>
<string name="settings_update_stable">Stable</string> <string name="settings_update_stable">Stable</string>
<string name="settings_update_beta">Beta</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> <string name="settings_update_custom_msg">Insert a custom URL</string>
<!--deprecated-->
<string name="settings_core_only_title">Magisk Core Only Mode</string> <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_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_magiskhide_summary">Hide Magisk from various forms of detection</string>
<string name="settings_hosts_title">Systemless hosts</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 <PreferenceCategory
android:key="general" android:key="general"
android:title="@string/settings_general_category"> android:title="@string/settings_general_category">
...@@ -131,4 +132,15 @@ ...@@ -131,4 +132,15 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory
android:key="redesign_cat"
app:title="Redesign">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="redesign"
app:title="Enable Redesign" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>
...@@ -16,7 +16,7 @@ buildscript { ...@@ -16,7 +16,7 @@ buildscript {
maven { url 'https://kotlin.bintray.com/kotlinx' } maven { url 'https://kotlin.bintray.com/kotlinx' }
} }
dependencies { 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}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${vKotlin}"
......
#Sun Jan 12 04:46:30 CST 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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