diff --git a/.gitattributes b/.gitattributes index 971dee287..ad2d62815 100644 --- a/.gitattributes +++ b/.gitattributes @@ -17,3 +17,4 @@ tools/** binary *.apk binary *.png binary *.jpg binary +*.ttf binary diff --git a/app/build.gradle b/app/build.gradle index 8d55ab87b..aecd21ea7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -19,6 +19,12 @@ android { multiDexEnabled true versionName props['appVersion'] versionCode props['appVersionCode'] as Integer + + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.incremental":"true"] + } + } } buildTypes { @@ -62,7 +68,7 @@ dependencies { implementation 'com.ncapdevi:frag-nav:3.2.0' implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6' - implementation 'io.reactivex.rxjava2:rxjava:2.2.13' + implementation 'io.reactivex.rxjava2:rxjava:2.2.16' implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' @@ -89,14 +95,16 @@ dependencies { implementation "org.koin:koin-android:${vKoin}" implementation "org.koin:koin-androidx-viewmodel:${vKoin}" - def vRetrofit = '2.6.2' + def vRetrofit = '2.7.1' implementation "com.squareup.retrofit2:retrofit:${vRetrofit}" implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}" implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}" implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}" - def vOkHttp = '3.12.6' - implementation "com.squareup.okhttp3:okhttp:${vOkHttp}" + def vOkHttp = '3.12.7' + implementation("com.squareup.okhttp3:okhttp:${vOkHttp}") { + force = true + } implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}" def vMoshi = '1.9.2' @@ -111,7 +119,7 @@ dependencies { replacedBy('com.github.topjohnwu:room-runtime') } } - def vRoom = '2.2.2' + def vRoom = '2.2.3' implementation "com.github.topjohnwu:room-runtime:${vRoom}" implementation "androidx.room:room-rxjava2:${vRoom}" kapt "androidx.room:room-compiler:${vRoom}" @@ -120,16 +128,16 @@ dependencies { implementation "androidx.navigation:navigation-fragment-ktx:${vNav}" implementation "androidx.navigation:navigation-ui-ktx:${vNav}" - implementation 'androidx.biometric:biometric:1.0.0' + implementation 'androidx.biometric:biometric:1.0.1' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03' + implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.fragment:fragment-ktx:1.2.0-rc03' - implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.fragment:fragment-ktx:1.2.0-rc05' implementation 'androidx.work:work-runtime:2.2.0' implementation 'androidx.transition:transition:1.3.0-rc02' implementation 'androidx.multidex:multidex:2.0.1' - implementation 'androidx.core:core-ktx:1.1.0' - implementation 'com.google.android.material:material:1.2.0-alpha02' + implementation 'androidx.core:core-ktx:1.2.0-rc01' + implementation 'com.google.android.material:material:1.2.0-alpha03' } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index accd5efc2..44ae7fed2 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -17,32 +17,26 @@ #} # Snet --keepclassmembers class com.topjohnwu.magisk.utils.SafetyNetHelper { *; } --keep,allowobfuscation interface com.topjohnwu.magisk.utils.SafetyNetHelper$Callback --keepclassmembers class * implements com.topjohnwu.magisk.utils.SafetyNetHelper$Callback { +-keepclassmembers class com.topjohnwu.magisk.core.utils.SafetyNetHelper { *; } +-keep,allowobfuscation interface com.topjohnwu.magisk.core.utils.SafetyNetHelper$Callback +-keepclassmembers class * implements com.topjohnwu.magisk.core.utils.SafetyNetHelper$Callback { void onResponse(int); } -# Keep all fragment constructors --keepclassmembers class * extends androidx.fragment.app.Fragment { - public (...); -} +# Fragments +-keep,allowobfuscation class * extends androidx.fragment.app.Fragment -# DelegateWorker --keep,allowobfuscation class * extends com.topjohnwu.magisk.base.DelegateWorker +# BaseWorkerWrapper +-keep,allowobfuscation class * extends com.topjohnwu.magisk.core.base.BaseWorkerWrapper # BootSigner -keep class a.a { *; } -# Workaround R8 bug --keep,allowobfuscation class com.topjohnwu.magisk.model.receiver.GeneralReceiver --keepclassmembers class a.e { *; } - # Strip logging -assumenosideeffects class timber.log.Timber.Tree { *; } # Excessive obfuscation --repackageclasses 'a' +-repackageclasses a -allowaccessmodification # QOL diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 46ed3d6da..033ceef1f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - @@ -21,6 +20,10 @@ + + + + diff --git a/app/src/main/java/a/a.java b/app/src/main/java/a/a.java index dcff5fbfa..7537dc297 100644 --- a/app/src/main/java/a/a.java +++ b/app/src/main/java/a/a.java @@ -1,6 +1,6 @@ package a; -import com.topjohnwu.magisk.utils.PatchAPK; +import com.topjohnwu.magisk.core.utils.PatchAPK; import com.topjohnwu.signing.BootSigner; public class a { diff --git a/app/src/main/java/a/b.java b/app/src/main/java/a/b.java deleted file mode 100644 index db9475e9c..000000000 --- a/app/src/main/java/a/b.java +++ /dev/null @@ -1,7 +0,0 @@ -package a; - -import com.topjohnwu.magisk.ui.MainActivity; - -public class b extends MainActivity { - /* stub */ -} diff --git a/app/src/main/java/a/c.java b/app/src/main/java/a/c.java deleted file mode 100644 index 73073ef6a..000000000 --- a/app/src/main/java/a/c.java +++ /dev/null @@ -1,7 +0,0 @@ -package a; - -import com.topjohnwu.magisk.ui.SplashActivity; - -public class c extends SplashActivity { - /* stub */ -} diff --git a/app/src/main/java/a/e.java b/app/src/main/java/a/e.java deleted file mode 100644 index e2f243e94..000000000 --- a/app/src/main/java/a/e.java +++ /dev/null @@ -1,13 +0,0 @@ -package a; - -import com.topjohnwu.magisk.App; - -public class e extends App { - public e() { - super(); - } - - public e(Object o) { - super(o); - } -} diff --git a/app/src/main/java/a/f.java b/app/src/main/java/a/f.java deleted file mode 100644 index 08d136b28..000000000 --- a/app/src/main/java/a/f.java +++ /dev/null @@ -1,7 +0,0 @@ -package a; - -import com.topjohnwu.magisk.ui.flash.FlashActivity; - -public class f extends FlashActivity { - /* stub */ -} diff --git a/app/src/main/java/a/g.java b/app/src/main/java/a/g.java index ceaf4ff41..e8b94881b 100644 --- a/app/src/main/java/a/g.java +++ b/app/src/main/java/a/g.java @@ -2,11 +2,11 @@ package a; import android.content.Context; -import com.topjohnwu.magisk.model.update.UpdateCheckService; - import androidx.annotation.NonNull; import androidx.work.WorkerParameters; +import com.topjohnwu.magisk.core.UpdateCheckService; + public class g extends w { /* Stub */ public g(@NonNull Context context, @NonNull WorkerParameters workerParams) { diff --git a/app/src/main/java/a/h.java b/app/src/main/java/a/h.java deleted file mode 100644 index 0be3819f7..000000000 --- a/app/src/main/java/a/h.java +++ /dev/null @@ -1,7 +0,0 @@ -package a; - -import com.topjohnwu.magisk.model.receiver.GeneralReceiver; - -public class h extends GeneralReceiver { - /* stub */ -} diff --git a/app/src/main/java/a/j.java b/app/src/main/java/a/j.java deleted file mode 100644 index a7be03859..000000000 --- a/app/src/main/java/a/j.java +++ /dev/null @@ -1,7 +0,0 @@ -package a; - -import com.topjohnwu.magisk.model.download.DownloadService; - -public class j extends DownloadService { - /* stub */ -} diff --git a/app/src/main/java/a/m.java b/app/src/main/java/a/m.java deleted file mode 100644 index 3d727085d..000000000 --- a/app/src/main/java/a/m.java +++ /dev/null @@ -1,7 +0,0 @@ -package a; - -import com.topjohnwu.magisk.ui.surequest.SuRequestActivity; - -public class m extends SuRequestActivity { - /* stub */ -} diff --git a/app/src/main/java/a/stubs.kt b/app/src/main/java/a/stubs.kt new file mode 100644 index 000000000..91dffbeb4 --- /dev/null +++ b/app/src/main/java/a/stubs.kt @@ -0,0 +1,55 @@ +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( + 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).newInstance() + base?.attachWorker(this) + } catch (e : java.lang.Exception) {} + } +} diff --git a/app/src/main/java/a/w.java b/app/src/main/java/a/w.java deleted file mode 100644 index f852a8968..000000000 --- a/app/src/main/java/a/w.java +++ /dev/null @@ -1,42 +0,0 @@ -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 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) ((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(); - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/base/BasePreferenceFragment.kt b/app/src/main/java/com/topjohnwu/magisk/base/BasePreferenceFragment.kt deleted file mode 100644 index 01aa437c0..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/base/BasePreferenceFragment.kt +++ /dev/null @@ -1,56 +0,0 @@ -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) - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/BaseViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/BaseViewModel.kt deleted file mode 100644 index 56338b1ce..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/BaseViewModel.kt +++ /dev/null @@ -1,35 +0,0 @@ -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(gIsConnected.value, gIsConnected) { - override fun get(): Boolean { - return gIsConnected.value - } - } - - fun withView(action: BaseActivity<*, *>.() -> Unit) { - ViewActionEvent(action).publish() - } - - fun withPermissions(vararg permissions: String): Observable { - val subject = PublishSubject.create() - return subject.doOnSubscribeUi { PermissionEvent(permissions.toList(), subject).publish() } - } - - fun back() = BackPressEvent().publish() - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/LoadingViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/LoadingViewModel.kt deleted file mode 100644 index bd4ac195d..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/LoadingViewModel.kt +++ /dev/null @@ -1,78 +0,0 @@ -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(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 Observable.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 Single.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 Maybe.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 Flowable.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 diff --git a/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/ObservableViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/ObservableViewModel.kt deleted file mode 100644 index 17ea6f373..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/ObservableViewModel.kt +++ /dev/null @@ -1,46 +0,0 @@ -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 diff --git a/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/StatefulViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/StatefulViewModel.kt deleted file mode 100644 index e441c84db..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/StatefulViewModel.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.topjohnwu.magisk.base.viewmodel - -abstract class StatefulViewModel>( - val defaultState: State -) : ObservableViewModel() { - - var state: State = defaultState - set(value) { - field = value - notifyStateChanged() - } - - open fun notifyStateChanged() = Unit - -} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/TeanityViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/TeanityViewModel.kt deleted file mode 100644 index 4fb10599a..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/TeanityViewModel.kt +++ /dev/null @@ -1,33 +0,0 @@ -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() - val viewEvents: LiveData get() = _viewEvents - - override fun onCleared() { - super.onCleared() - disposables.clear() - } - - fun Event.publish() { - _viewEvents.value = this - } - - fun Int.publish() { - _viewEvents.value = SimpleViewEvent(this) - } - - fun Disposable.add() { - disposables.add(this) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/App.kt b/app/src/main/java/com/topjohnwu/magisk/core/App.kt similarity index 83% rename from app/src/main/java/com/topjohnwu/magisk/App.kt rename to app/src/main/java/com/topjohnwu/magisk/core/App.kt index 3603a01ea..89d40442c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/App.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/App.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk +package com.topjohnwu.magisk.core import android.app.Application import android.content.Context @@ -9,6 +9,12 @@ import androidx.room.Room import androidx.work.WorkManager import androidx.work.impl.WorkDatabase import androidx.work.impl.WorkDatabase_Impl +import com.topjohnwu.magisk.BuildConfig +import com.topjohnwu.magisk.DynAPK +import com.topjohnwu.magisk.FileProvider +import com.topjohnwu.magisk.core.su.SuCallbackHandler +import com.topjohnwu.magisk.core.utils.RootInit +import com.topjohnwu.magisk.core.utils.updateConfig import com.topjohnwu.magisk.data.database.RepoDatabase import com.topjohnwu.magisk.data.database.RepoDatabase_Impl import com.topjohnwu.magisk.data.database.SuLogDatabase @@ -17,13 +23,11 @@ import com.topjohnwu.magisk.di.ActivityTracker import com.topjohnwu.magisk.di.koinModules import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.unwrap -import com.topjohnwu.magisk.utils.RootInit -import com.topjohnwu.magisk.utils.SuHandler -import com.topjohnwu.magisk.utils.updateConfig import com.topjohnwu.superuser.Shell import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin import timber.log.Timber +import kotlin.system.exitProcess open class App() : Application() { @@ -37,7 +41,7 @@ open class App() : Application() { Shell.Config.verboseLogging(BuildConfig.DEBUG) Shell.Config.addInitializers(RootInit::class.java) Shell.Config.setTimeout(2) - FileProvider.callHandler = SuHandler + FileProvider.callHandler = SuCallbackHandler Room.setFactory { when (it) { WorkDatabase::class.java -> WorkDatabase_Impl() @@ -46,13 +50,19 @@ open class App() : Application() { else -> null } } + + // Always log full stack trace with Timber + Timber.plant(Timber.DebugTree()) + Thread.setDefaultUncaughtExceptionHandler { _, e -> + Timber.e(e) + exitProcess(1) + } } override fun attachBaseContext(base: Context) { // Basic setup if (BuildConfig.DEBUG) MultiDex.install(base) - Timber.plant(Timber.DebugTree()) // Some context magic val app: Application diff --git a/app/src/main/java/com/topjohnwu/magisk/Config.kt b/app/src/main/java/com/topjohnwu/magisk/core/Config.kt similarity index 79% rename from app/src/main/java/com/topjohnwu/magisk/Config.kt rename to app/src/main/java/com/topjohnwu/magisk/core/Config.kt index 22a1ef47b..118412372 100644 --- a/app/src/main/java/com/topjohnwu/magisk/Config.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/Config.kt @@ -1,19 +1,22 @@ -package com.topjohnwu.magisk +package com.topjohnwu.magisk.core import android.content.Context import android.content.SharedPreferences import android.os.Environment import android.util.Xml +import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.edit -import com.topjohnwu.magisk.data.database.SettingsDao -import com.topjohnwu.magisk.data.database.StringDao +import com.topjohnwu.magisk.BuildConfig +import com.topjohnwu.magisk.core.magiskdb.SettingsDao +import com.topjohnwu.magisk.core.magiskdb.StringDao import com.topjohnwu.magisk.data.repository.DBConfig import com.topjohnwu.magisk.di.Protected import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.magisk.model.preference.PreferenceModel -import com.topjohnwu.magisk.utils.BiometricHelper -import com.topjohnwu.magisk.utils.Utils +import com.topjohnwu.magisk.ui.theme.Theme +import com.topjohnwu.magisk.core.utils.BiometricHelper +import com.topjohnwu.magisk.core.utils.Utils import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.io.SuFile import com.topjohnwu.superuser.io.SuFileInputStream @@ -45,10 +48,15 @@ object Config : PreferenceModel, DBConfig { const val CUSTOM_CHANNEL = "custom_channel" const val LOCALE = "locale" const val DARK_THEME = "dark_theme" + const val DARK_THEME_EXTENDED = "dark_theme_extended" const val REPO_ORDER = "repo_order" const val SHOW_SYSTEM_APP = "show_system" const val DOWNLOAD_PATH = "download_path" + const val REDESIGN = "redesign" + const val SAFETY = "safety_notice" + const val THEME_ORDINAL = "theme_ordinal" const val BOOT_ID = "boot_id" + const val LIST_SPAN_COUNT = "list_span_count" // system state const val MAGISKHIDE = "magiskhide" @@ -103,39 +111,67 @@ object Config : PreferenceModel, DBConfig { Value.CANARY_DEBUG_CHANNEL else Value.CANARY_CHANNEL - } - else Value.DEFAULT_CHANNEL + } else Value.DEFAULT_CHANNEL var bootId by preference(Key.BOOT_ID, "") var downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS) - var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE) + var repoOrder by preference( + Key.REPO_ORDER, + Value.ORDER_DATE + ) var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10) - var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT) - var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST) - var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel) + var suAutoReponse by preferenceStrInt( + Key.SU_AUTO_RESPONSE, + Value.SU_PROMPT + ) + var suNotification by preferenceStrInt( + Key.SU_NOTIFICATION, + Value.NOTIFICATION_TOAST + ) + var updateChannel by preferenceStrInt( + Key.UPDATE_CHANNEL, + defaultChannel + ) - var darkTheme by preference(Key.DARK_THEME, true) + var safetyNotice by preference(Key.SAFETY, true) + var darkThemeExtended by preference( + Key.DARK_THEME_EXTENDED, + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + ) + var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal) var suReAuth by preference(Key.SU_REAUTH, false) var checkUpdate by preference(Key.CHECK_UPDATES, true) var magiskHide by preference(Key.MAGISKHIDE, true) + @JvmStatic var coreOnly by preference(Key.COREONLY, false) var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false) + var listSpanCount by preference(Key.LIST_SPAN_COUNT, 2) var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "") var locale by preference(Key.LOCALE, "") - var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB) - var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER) - var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY) + var rootMode by dbSettings( + Key.ROOT_ACCESS, + Value.ROOT_ACCESS_APPS_AND_ADB + ) + var suMntNamespaceMode by dbSettings( + Key.SU_MNT_NS, + Value.NAMESPACE_MODE_REQUESTER + ) + var suMultiuserMode by dbSettings( + Key.SU_MULTIUSER_MODE, + Value.MULTIUSER_MODE_OWNER_ONLY + ) var suBiometric by dbSettings(Key.SU_BIOMETRIC, false) var suManager by dbStrings(Key.SU_MANAGER, "", true) var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true) // Always return a path in external storage where we can write - val downloadDirectory get() = - Utils.ensureDownloadPath(downloadPath) ?: get().getExternalFilesDir(null)!! + val downloadDirectory + get() = + Utils.ensureDownloadPath(downloadPath) ?: get().getExternalFilesDir(null)!! private const val SU_FINGERPRINT = "su_fingerprint" @@ -163,7 +199,9 @@ object Config : PreferenceModel, DBConfig { } private fun parsePrefs(editor: SharedPreferences.Editor) = editor.apply { - val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS) + val config = SuFile.open("/data/adb", + Const.MANAGER_CONFIGS + ) if (config.exists()) runCatching { val input = SuFileInputStream(config) val parser = Xml.newPullParser() diff --git a/app/src/main/java/com/topjohnwu/magisk/Const.kt b/app/src/main/java/com/topjohnwu/magisk/core/Const.kt similarity index 97% rename from app/src/main/java/com/topjohnwu/magisk/Const.kt rename to app/src/main/java/com/topjohnwu/magisk/core/Const.kt index 912d87ccb..1ac63c3a7 100644 --- a/app/src/main/java/com/topjohnwu/magisk/Const.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/Const.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk +package com.topjohnwu.magisk.core import android.os.Process import java.io.File @@ -62,6 +62,7 @@ object Const { const val ETAG_KEY = "ETag" // intents const val OPEN_SECTION = "section" + const val OPEN_SETTINGS = "settings" const val INTENT_SET_APP = "app_json" const val FLASH_ACTION = "action" const val FLASH_DATA = "additional_data" diff --git a/app/src/main/java/com/topjohnwu/magisk/model/receiver/GeneralReceiver.kt b/app/src/main/java/com/topjohnwu/magisk/core/GeneralReceiver.kt similarity index 75% rename from app/src/main/java/com/topjohnwu/magisk/model/receiver/GeneralReceiver.kt rename to app/src/main/java/com/topjohnwu/magisk/core/GeneralReceiver.kt index 90134a8eb..ac0c384fe 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/receiver/GeneralReceiver.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/GeneralReceiver.kt @@ -1,19 +1,16 @@ -package com.topjohnwu.magisk.model.receiver +package com.topjohnwu.magisk.core import android.content.ContextWrapper import android.content.Intent -import com.topjohnwu.magisk.Config -import com.topjohnwu.magisk.Const -import com.topjohnwu.magisk.Info -import com.topjohnwu.magisk.base.BaseReceiver -import com.topjohnwu.magisk.data.database.PolicyDao +import com.topjohnwu.magisk.core.base.BaseReceiver +import com.topjohnwu.magisk.core.download.DownloadService +import com.topjohnwu.magisk.core.magiskdb.PolicyDao +import com.topjohnwu.magisk.core.model.ManagerJson +import com.topjohnwu.magisk.core.su.SuCallbackHandler +import com.topjohnwu.magisk.core.view.Shortcuts import com.topjohnwu.magisk.extensions.reboot -import com.topjohnwu.magisk.model.download.DownloadService -import com.topjohnwu.magisk.model.entity.ManagerJson import com.topjohnwu.magisk.model.entity.internal.Configuration import com.topjohnwu.magisk.model.entity.internal.DownloadSubject -import com.topjohnwu.magisk.utils.SuHandler -import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.superuser.Shell import org.koin.core.inject @@ -30,7 +27,7 @@ open class GeneralReceiver : BaseReceiver() { when (intent.action ?: return) { Intent.ACTION_REBOOT -> { - SuHandler(context, intent.getStringExtra("action"), intent.extras) + SuCallbackHandler(context, intent.getStringExtra("action"), intent.extras) } Intent.ACTION_PACKAGE_REPLACED -> { // This will only work pre-O diff --git a/app/src/main/java/com/topjohnwu/magisk/Hacks.kt b/app/src/main/java/com/topjohnwu/magisk/core/Hacks.kt similarity index 86% rename from app/src/main/java/com/topjohnwu/magisk/Hacks.kt rename to app/src/main/java/com/topjohnwu/magisk/core/Hacks.kt index c1e0c735f..ec22ce036 100644 --- a/app/src/main/java/com/topjohnwu/magisk/Hacks.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/Hacks.kt @@ -1,6 +1,6 @@ @file:Suppress("DEPRECATION") -package com.topjohnwu.magisk +package com.topjohnwu.magisk.core import android.annotation.SuppressLint import android.app.job.JobInfo @@ -14,23 +14,22 @@ import android.content.res.AssetManager import android.content.res.Configuration import android.content.res.Resources import androidx.annotation.RequiresApi +import com.topjohnwu.magisk.DynAPK +import com.topjohnwu.magisk.ProcessPhoenix +import com.topjohnwu.magisk.core.download.DownloadService +import com.topjohnwu.magisk.core.utils.refreshLocale +import com.topjohnwu.magisk.core.utils.updateConfig import com.topjohnwu.magisk.extensions.forceGetDeclaredField -import com.topjohnwu.magisk.model.download.DownloadService -import com.topjohnwu.magisk.model.receiver.GeneralReceiver -import com.topjohnwu.magisk.model.update.UpdateCheckService +import com.topjohnwu.magisk.legacy.flash.FlashActivity +import com.topjohnwu.magisk.legacy.surequest.SuRequestActivity import com.topjohnwu.magisk.ui.MainActivity -import com.topjohnwu.magisk.ui.SplashActivity -import com.topjohnwu.magisk.ui.flash.FlashActivity -import com.topjohnwu.magisk.ui.surequest.SuRequestActivity -import com.topjohnwu.magisk.utils.refreshLocale -import com.topjohnwu.magisk.utils.updateConfig fun AssetManager.addAssetPath(path: String) { DynAPK.addAssetPath(this, path) } -fun Context.wrap(global: Boolean = true): Context - = if (global) GlobalResContext(this) else ResContext(this) +fun Context.wrap(global: Boolean = true): Context = + if (global) GlobalResContext(this) else ResContext(this) fun Context.wrapJob(): Context = object : GlobalResContext(this) { @@ -130,7 +129,8 @@ private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler val name = service.className val component = ComponentName( service.packageName, - Info.stub!!.classToComponent[name] ?: name) + Info.stub!!.classToComponent[name] ?: name + ) javaClass.forceGetDeclaredField("service")?.set(this, component) return this diff --git a/app/src/main/java/com/topjohnwu/magisk/Info.kt b/app/src/main/java/com/topjohnwu/magisk/core/Info.kt similarity index 92% rename from app/src/main/java/com/topjohnwu/magisk/Info.kt rename to app/src/main/java/com/topjohnwu/magisk/core/Info.kt index b95834dc0..c4a9c7061 100644 --- a/app/src/main/java/com/topjohnwu/magisk/Info.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/Info.kt @@ -1,9 +1,10 @@ -package com.topjohnwu.magisk +package com.topjohnwu.magisk.core import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork +import com.topjohnwu.magisk.DynAPK +import com.topjohnwu.magisk.core.model.UpdateInfo import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.subscribeK -import com.topjohnwu.magisk.model.entity.UpdateInfo import com.topjohnwu.magisk.utils.CachedValue import com.topjohnwu.magisk.utils.KObservableField import com.topjohnwu.superuser.Shell @@ -17,12 +18,16 @@ object Info { val envRef = CachedValue { loadState() } + @JvmStatic val env by envRef // Local var remote = UpdateInfo() // Remote var stub: DynAPK.Data? = null // Stub + @JvmStatic var keepVerity = false + @JvmStatic var keepEnc = false + @JvmStatic var recovery = false val isConnected by lazy { diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt b/app/src/main/java/com/topjohnwu/magisk/core/SplashActivity.kt similarity index 86% rename from app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt rename to app/src/main/java/com/topjohnwu/magisk/core/SplashActivity.kt index 3a4555976..d56489ff1 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/SplashActivity.kt @@ -1,12 +1,13 @@ -package com.topjohnwu.magisk.ui +package com.topjohnwu.magisk.core import android.app.Activity import android.content.Context import android.os.Bundle -import com.topjohnwu.magisk.* -import com.topjohnwu.magisk.utils.Utils -import com.topjohnwu.magisk.view.Notifications -import com.topjohnwu.magisk.view.Shortcuts +import com.topjohnwu.magisk.BuildConfig +import com.topjohnwu.magisk.model.navigation.Navigation +import com.topjohnwu.magisk.core.utils.Utils +import com.topjohnwu.magisk.core.view.Notifications +import com.topjohnwu.magisk.core.view.Shortcuts import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ShellUtils @@ -61,8 +62,7 @@ open class SplashActivity : Activity() { } DONE = true - - startActivity(intent().apply { intent?.also { putExtras(it) } }) + Navigation.start(intent, this) finish() } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/update/UpdateCheckService.kt b/app/src/main/java/com/topjohnwu/magisk/core/UpdateCheckService.kt similarity index 80% rename from app/src/main/java/com/topjohnwu/magisk/model/update/UpdateCheckService.kt rename to app/src/main/java/com/topjohnwu/magisk/core/UpdateCheckService.kt index 190e05802..162918caa 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/update/UpdateCheckService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/UpdateCheckService.kt @@ -1,15 +1,14 @@ -package com.topjohnwu.magisk.model.update +package com.topjohnwu.magisk.core import androidx.work.ListenableWorker import com.topjohnwu.magisk.BuildConfig -import com.topjohnwu.magisk.Info -import com.topjohnwu.magisk.base.DelegateWorker +import com.topjohnwu.magisk.core.base.BaseWorkerWrapper +import com.topjohnwu.magisk.core.view.Notifications import com.topjohnwu.magisk.data.repository.MagiskRepository import com.topjohnwu.magisk.extensions.inject -import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.superuser.Shell -class UpdateCheckService : DelegateWorker() { +class UpdateCheckService : BaseWorkerWrapper() { private val magiskRepo: MagiskRepository by inject() diff --git a/app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt b/app/src/main/java/com/topjohnwu/magisk/core/base/BaseActivity.kt similarity index 57% rename from app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt rename to app/src/main/java/com/topjohnwu/magisk/core/base/BaseActivity.kt index 0c4435dee..3c08cebb4 100644 --- a/app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/base/BaseActivity.kt @@ -1,51 +1,26 @@ -package com.topjohnwu.magisk.base +package com.topjohnwu.magisk.core.base import android.Manifest import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration -import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate import androidx.collection.SparseArrayCompat import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import androidx.databinding.DataBindingUtil -import androidx.databinding.ViewDataBinding -import com.topjohnwu.magisk.BR -import com.topjohnwu.magisk.Config -import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.base.viewmodel.BaseViewModel +import com.topjohnwu.magisk.core.utils.currentLocale +import com.topjohnwu.magisk.core.wrap import com.topjohnwu.magisk.extensions.set -import com.topjohnwu.magisk.model.events.EventHandler import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder -import com.topjohnwu.magisk.utils.currentLocale -import com.topjohnwu.magisk.wrap import kotlin.random.Random -typealias RequestCallback = BaseActivity<*, *>.(Int, Intent?) -> Unit +typealias RequestCallback = BaseActivity.(Int, Intent?) -> Unit -abstract class BaseActivity : - AppCompatActivity(), EventHandler { - - protected lateinit var binding: Binding - protected abstract val layoutRes: Int - protected abstract val viewModel: ViewModel - protected open val themeRes: Int = R.style.MagiskTheme - protected open val snackbarView get() = binding.root +abstract class BaseActivity : AppCompatActivity() { private val resultCallbacks by lazy { SparseArrayCompat() } - init { - val theme = if (Config.darkTheme) { - AppCompatDelegate.MODE_NIGHT_YES - } else { - AppCompatDelegate.MODE_NIGHT_NO - } - AppCompatDelegate.setDefaultNightMode(theme) - } - override fun applyOverrideConfiguration(config: Configuration?) { // Force applying our preferred local config?.setLocale(currentLocale) @@ -56,18 +31,6 @@ abstract class BaseActivity(this, layoutRes).apply { - setVariable(BR.viewModel, viewModel) - lifecycleOwner = this@BaseActivity - } - } - fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) { val request = PermissionRequestBuilder().apply(builder).build() val ungranted = permissions.filter { @@ -93,7 +56,7 @@ abstract class BaseActivity, grantResults: IntArray) { + requestCode: Int, permissions: Array, grantResults: IntArray) { var success = true for (res in grantResults) { if (res != PackageManager.PERMISSION_GRANTED) { @@ -101,18 +64,18 @@ abstract class BaseActivity - notification.setContentText(getString(R.string.download_file_error)) - .setSmallIcon(android.R.drawable.stat_notify_error) - .setOngoing(false) - } + failNotify(subject) }) { val newId = finishNotify(subject) if (get() !is NullActivity) { @@ -62,12 +61,12 @@ abstract class RemoteFileService : NotificationService() { } private fun download(subject: DownloadSubject) = service.fetchFile(subject.url) - .map { it.toStream(subject.hashCode()) } + .map { it.toStream(subject.hashCode(), subject) } .flatMapCompletable { stream -> when (subject) { is Module -> service.fetchInstaller() - .doOnSuccess { stream.toModule(subject.file, it.byteStream()) } - .ignoreElement() + .doOnSuccess { stream.toModule(subject.file, it.byteStream()) } + .ignoreElement() else -> Completable.fromAction { stream.writeTo(subject.file) } } }.doOnComplete { @@ -75,7 +74,7 @@ abstract class RemoteFileService : NotificationService() { handleAPK(subject) } - private fun ResponseBody.toStream(id: Int): InputStream { + private fun ResponseBody.toStream(id: Int, subject: DownloadSubject): InputStream { val maxRaw = contentLength() val max = maxRaw / 1_000_000f @@ -83,17 +82,27 @@ abstract class RemoteFileService : NotificationService() { val progress = it / 1_000_000f update(id) { notification -> if (maxRaw > 0) { + send(progress / max, subject) notification - .setProgress(maxRaw.toInt(), it.toInt(), false) - .setContentText("%.2f / %.2f MB".format(progress, max)) + .setProgress(maxRaw.toInt(), it.toInt(), false) + .setContentText("%.2f / %.2f MB".format(progress, max)) } else { + send(-1f, subject) notification.setContentText("%.2f MB / ??".format(progress)) } } } } + private fun failNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) { + send(0f, subject) + it.setContentText(getString(R.string.download_file_error)) + .setSmallIcon(android.R.drawable.stat_notify_error) + .setOngoing(false) + } + private fun finishNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) { + send(1f, subject) it.addActions(subject) .setContentText(getString(R.string.download_complete)) .setSmallIcon(android.R.drawable.stat_sys_download_done) @@ -111,8 +120,19 @@ abstract class RemoteFileService : NotificationService() { protected abstract fun Notification.Builder.addActions(subject: DownloadSubject) : Notification.Builder - companion object { + companion object : KoinComponent { const val ARG_URL = "arg_url" + + private val internalProgressBroadcast = MutableLiveData>() + val progressBroadcast: LiveData> get() = internalProgressBroadcast + + fun send(progress: Float, subject: DownloadSubject) { + internalProgressBroadcast.postValue(progress to subject) + } + + fun reset() { + internalProgressBroadcast.value = null + } } } diff --git a/app/src/main/java/com/topjohnwu/magisk/data/database/magiskdb/BaseDao.kt b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/BaseDao.kt similarity index 95% rename from app/src/main/java/com/topjohnwu/magisk/data/database/magiskdb/BaseDao.kt rename to app/src/main/java/com/topjohnwu/magisk/core/magiskdb/BaseDao.kt index 93f6c7aa1..0bc976b77 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/database/magiskdb/BaseDao.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/BaseDao.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.data.database.magiskdb +package com.topjohnwu.magisk.core.magiskdb import androidx.annotation.StringDef import com.topjohnwu.superuser.Shell diff --git a/app/src/main/java/com/topjohnwu/magisk/data/database/PolicyDao.kt b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/PolicyDao.kt similarity index 79% rename from app/src/main/java/com/topjohnwu/magisk/data/database/PolicyDao.kt rename to app/src/main/java/com/topjohnwu/magisk/core/magiskdb/PolicyDao.kt index 653b12e7a..a1aab0560 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/database/PolicyDao.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/PolicyDao.kt @@ -1,16 +1,12 @@ -package com.topjohnwu.magisk.data.database +package com.topjohnwu.magisk.core.magiskdb import android.content.Context import android.content.pm.PackageManager -import com.topjohnwu.magisk.Const -import com.topjohnwu.magisk.data.database.magiskdb.BaseDao -import com.topjohnwu.magisk.data.database.magiskdb.Delete -import com.topjohnwu.magisk.data.database.magiskdb.Replace -import com.topjohnwu.magisk.data.database.magiskdb.Select +import com.topjohnwu.magisk.core.Const +import com.topjohnwu.magisk.core.model.MagiskPolicy +import com.topjohnwu.magisk.core.model.toMap +import com.topjohnwu.magisk.core.model.toPolicy import com.topjohnwu.magisk.extensions.now -import com.topjohnwu.magisk.model.entity.MagiskPolicy -import com.topjohnwu.magisk.model.entity.toMap -import com.topjohnwu.magisk.model.entity.toPolicy import timber.log.Timber import java.util.concurrent.TimeUnit diff --git a/app/src/main/java/com/topjohnwu/magisk/data/database/magiskdb/Query.kt b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/Query.kt similarity index 98% rename from app/src/main/java/com/topjohnwu/magisk/data/database/magiskdb/Query.kt rename to app/src/main/java/com/topjohnwu/magisk/core/magiskdb/Query.kt index d34542296..6f7d53b81 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/database/magiskdb/Query.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/Query.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.data.database.magiskdb +package com.topjohnwu.magisk.core.magiskdb import androidx.annotation.StringDef diff --git a/app/src/main/java/com/topjohnwu/magisk/data/database/SettingsDao.kt b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/SettingsDao.kt similarity index 65% rename from app/src/main/java/com/topjohnwu/magisk/data/database/SettingsDao.kt rename to app/src/main/java/com/topjohnwu/magisk/core/magiskdb/SettingsDao.kt index fdc00d520..197c71ede 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/database/SettingsDao.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/SettingsDao.kt @@ -1,9 +1,4 @@ -package com.topjohnwu.magisk.data.database - -import com.topjohnwu.magisk.data.database.magiskdb.BaseDao -import com.topjohnwu.magisk.data.database.magiskdb.Delete -import com.topjohnwu.magisk.data.database.magiskdb.Replace -import com.topjohnwu.magisk.data.database.magiskdb.Select +package com.topjohnwu.magisk.core.magiskdb class SettingsDao : BaseDao() { diff --git a/app/src/main/java/com/topjohnwu/magisk/data/database/StringDao.kt b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/StringDao.kt similarity index 64% rename from app/src/main/java/com/topjohnwu/magisk/data/database/StringDao.kt rename to app/src/main/java/com/topjohnwu/magisk/core/magiskdb/StringDao.kt index 24eab0564..0fedaa414 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/database/StringDao.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/magiskdb/StringDao.kt @@ -1,9 +1,4 @@ -package com.topjohnwu.magisk.data.database - -import com.topjohnwu.magisk.data.database.magiskdb.BaseDao -import com.topjohnwu.magisk.data.database.magiskdb.Delete -import com.topjohnwu.magisk.data.database.magiskdb.Replace -import com.topjohnwu.magisk.data.database.magiskdb.Select +package com.topjohnwu.magisk.core.magiskdb class StringDao : BaseDao() { diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/MagiskPolicy.kt b/app/src/main/java/com/topjohnwu/magisk/core/model/MagiskPolicy.kt similarity index 94% rename from app/src/main/java/com/topjohnwu/magisk/model/entity/MagiskPolicy.kt rename to app/src/main/java/com/topjohnwu/magisk/core/model/MagiskPolicy.kt index b11304ab3..aebdd20f4 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/MagiskPolicy.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/model/MagiskPolicy.kt @@ -1,9 +1,9 @@ -package com.topjohnwu.magisk.model.entity +package com.topjohnwu.magisk.core.model import android.content.pm.ApplicationInfo import android.content.pm.PackageManager +import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.INTERACTIVE import com.topjohnwu.magisk.extensions.getLabel -import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE data class MagiskPolicy( diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/UpdateInfo.kt b/app/src/main/java/com/topjohnwu/magisk/core/model/UpdateInfo.kt similarity index 95% rename from app/src/main/java/com/topjohnwu/magisk/model/entity/UpdateInfo.kt rename to app/src/main/java/com/topjohnwu/magisk/core/model/UpdateInfo.kt index d7b75580b..5fe144bdb 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/UpdateInfo.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/model/UpdateInfo.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.model.entity +package com.topjohnwu.magisk.core.model import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/module/BaseModule.kt b/app/src/main/java/com/topjohnwu/magisk/core/model/module/BaseModule.kt similarity index 96% rename from app/src/main/java/com/topjohnwu/magisk/model/entity/module/BaseModule.kt rename to app/src/main/java/com/topjohnwu/magisk/core/model/module/BaseModule.kt index 55004495a..601447e6c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/module/BaseModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/model/module/BaseModule.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.model.entity.module +package com.topjohnwu.magisk.core.model.module abstract class BaseModule : Comparable { abstract var id: String diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/module/Module.kt b/app/src/main/java/com/topjohnwu/magisk/core/model/module/Module.kt similarity index 96% rename from app/src/main/java/com/topjohnwu/magisk/model/entity/module/Module.kt rename to app/src/main/java/com/topjohnwu/magisk/core/model/module/Module.kt index 1da190682..40ddecdd7 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/module/Module.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/model/module/Module.kt @@ -1,7 +1,7 @@ -package com.topjohnwu.magisk.model.entity.module +package com.topjohnwu.magisk.core.model.module import androidx.annotation.WorkerThread -import com.topjohnwu.magisk.Const +import com.topjohnwu.magisk.core.Const import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.io.SuFile diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/module/Repo.kt b/app/src/main/java/com/topjohnwu/magisk/core/model/module/Repo.kt similarity index 96% rename from app/src/main/java/com/topjohnwu/magisk/model/entity/module/Repo.kt rename to app/src/main/java/com/topjohnwu/magisk/core/model/module/Repo.kt index 3bb4ee096..2bd4f1aaf 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/module/Repo.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/model/module/Repo.kt @@ -1,9 +1,9 @@ -package com.topjohnwu.magisk.model.entity.module +package com.topjohnwu.magisk.core.model.module import android.os.Parcelable import androidx.room.Entity import androidx.room.PrimaryKey -import com.topjohnwu.magisk.Const +import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.data.repository.StringRepository import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.legalFilename diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/SuHandler.kt b/app/src/main/java/com/topjohnwu/magisk/core/su/SuCallbackHandler.kt similarity index 88% rename from app/src/main/java/com/topjohnwu/magisk/utils/SuHandler.kt rename to app/src/main/java/com/topjohnwu/magisk/core/su/SuCallbackHandler.kt index c009b2445..6b9959566 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/SuHandler.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/su/SuCallbackHandler.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.utils +package com.topjohnwu.magisk.core.su import android.content.Context import android.content.Intent @@ -6,20 +6,26 @@ import android.os.Build import android.os.Bundle import android.os.Process import android.widget.Toast -import com.topjohnwu.magisk.* +import com.topjohnwu.magisk.BuildConfig +import com.topjohnwu.magisk.ProviderCallHandler +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.core.Config +import com.topjohnwu.magisk.core.intent +import com.topjohnwu.magisk.core.model.MagiskPolicy +import com.topjohnwu.magisk.core.model.toPolicy +import com.topjohnwu.magisk.core.wrap import com.topjohnwu.magisk.data.repository.LogRepository import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.startActivity import com.topjohnwu.magisk.extensions.startActivityWithRoot import com.topjohnwu.magisk.extensions.subscribeK -import com.topjohnwu.magisk.model.entity.MagiskPolicy +import com.topjohnwu.magisk.legacy.surequest.SuRequestActivity import com.topjohnwu.magisk.model.entity.toLog -import com.topjohnwu.magisk.model.entity.toPolicy -import com.topjohnwu.magisk.ui.surequest.SuRequestActivity +import com.topjohnwu.magisk.core.utils.Utils import com.topjohnwu.superuser.Shell import timber.log.Timber -object SuHandler : ProviderCallHandler { +object SuCallbackHandler : ProviderCallHandler { const val REQUEST = "request" const val LOG = "log" diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/SuConnector.kt b/app/src/main/java/com/topjohnwu/magisk/core/su/SuConnector.kt similarity index 97% rename from app/src/main/java/com/topjohnwu/magisk/utils/SuConnector.kt rename to app/src/main/java/com/topjohnwu/magisk/core/su/SuConnector.kt index 9570d99da..0b7e8ee2c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/SuConnector.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/su/SuConnector.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.utils +package com.topjohnwu.magisk.core.su import android.net.LocalSocket import android.net.LocalSocketAddress diff --git a/app/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt b/app/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt new file mode 100644 index 000000000..204ffc31b --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt @@ -0,0 +1,103 @@ +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() + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/tasks/FlashZip.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashZip.kt similarity index 95% rename from app/src/main/java/com/topjohnwu/magisk/tasks/FlashZip.kt rename to app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashZip.kt index 2e9e9ac36..22759ece6 100644 --- a/app/src/main/java/com/topjohnwu/magisk/tasks/FlashZip.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashZip.kt @@ -1,13 +1,13 @@ -package com.topjohnwu.magisk.tasks +package com.topjohnwu.magisk.core.tasks import android.content.Context import android.net.Uri -import com.topjohnwu.magisk.Const +import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.extensions.fileName import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.magisk.extensions.readUri import com.topjohnwu.magisk.extensions.subscribeK -import com.topjohnwu.magisk.utils.unzip +import com.topjohnwu.magisk.core.utils.unzip import com.topjohnwu.superuser.Shell import io.reactivex.Single import java.io.File diff --git a/app/src/main/java/com/topjohnwu/magisk/tasks/MagiskInstaller.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt similarity index 89% rename from app/src/main/java/com/topjohnwu/magisk/tasks/MagiskInstaller.kt rename to app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt index 19598e116..8c148df71 100644 --- a/app/src/main/java/com/topjohnwu/magisk/tasks/MagiskInstaller.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.tasks +package com.topjohnwu.magisk.core.tasks import android.content.Context import android.net.Uri @@ -6,11 +6,13 @@ import android.os.Build import android.text.TextUtils import androidx.annotation.MainThread import androidx.annotation.WorkerThread -import com.topjohnwu.magisk.Config -import com.topjohnwu.magisk.Info +import androidx.core.net.toUri +import com.topjohnwu.magisk.core.Info +import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.di.Protected import com.topjohnwu.magisk.extensions.* +import com.topjohnwu.magisk.net.Networking import com.topjohnwu.signing.SignBoot import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ShellUtils @@ -34,10 +36,10 @@ import java.util.zip.ZipInputStream abstract class MagiskInstaller { - protected lateinit var srcBoot: String - protected lateinit var destFile: File protected lateinit var installDir: File - protected lateinit var zipUri: Uri + private lateinit var srcBoot: String + private lateinit var destFile: File + private lateinit var zipUri: Uri private val console: MutableList private val logs: MutableList @@ -60,7 +62,7 @@ abstract class MagiskInstaller { installDir.mkdirs() } - protected fun findImage(): Boolean { + private fun findImage(): Boolean { srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh() if (srcBoot.isEmpty()) { console.add("! Unable to detect target image") @@ -70,7 +72,7 @@ abstract class MagiskInstaller { return true } - protected fun findSecondaryImage(): Boolean { + private fun findSecondaryImage(): Boolean { val slot = "echo \$SLOT".fsh() val target = if (slot == "_a") "_b" else "_a" console.add("- Target slot: $target") @@ -87,7 +89,7 @@ abstract class MagiskInstaller { return true } - protected fun extractZip(): Boolean { + private fun extractZip(): Boolean { val arch: String arch = if (Build.VERSION.SDK_INT >= 21) { val abis = listOf(*Build.SUPPORTED_ABIS) @@ -208,7 +210,7 @@ abstract class MagiskInstaller { } } - protected fun handleFile(uri: Uri): Boolean { + private fun handleFile(uri: Uri): Boolean { try { context.readUri(uri).buffered().use { it.mark(500) @@ -238,7 +240,7 @@ abstract class MagiskInstaller { return true } - protected fun patchBoot(): Boolean { + private fun patchBoot(): Boolean { var isSigned = false try { SuFileInputStream(srcBoot).use { @@ -284,7 +286,7 @@ abstract class MagiskInstaller { return true } - protected fun flashBoot(): Boolean { + private fun flashBoot(): Boolean { if (!"direct_install $installDir $srcBoot".sh().isSuccess) return false arrayOf( @@ -294,7 +296,7 @@ abstract class MagiskInstaller { return true } - protected fun storeBoot(): Boolean { + private fun storeBoot(): Boolean { val patched = SuFile.open(installDir, "new-boot.img") try { val os = tarOut?.let { @@ -320,7 +322,7 @@ abstract class MagiskInstaller { return true } - protected fun postOTA(): Boolean { + private fun postOTA(): Boolean { val bootctl = SuFile("/data/adb/bootctl") try { withStreams(service.fetchBootctl().blockingGet().byteStream(), bootctl.suOutputStream()) { @@ -345,6 +347,28 @@ abstract class MagiskInstaller { private fun String.fsh() = ShellUtils.fastCmd(this) private fun Array.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() + val zip: File = context.cachedFile("magisk.zip") + + installDir = SuFile("/data/adb/magisk") + Shell.su("rm -rf /data/adb/magisk/*").exec() + + if (!ShellUtils.checkSum("MD5", zip, Info.remote.magisk.md5)) + Networking.get(Info.remote.magisk.link).execForFile(zip) + + zipUri = zip.toUri() + return extractZip() && Shell.su("fix_env").exec().isSuccess + } + @WorkerThread protected abstract fun operations(): Boolean diff --git a/app/src/main/java/com/topjohnwu/magisk/core/tasks/RepoUpdater.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/RepoUpdater.kt new file mode 100644 index 000000000..b42147228 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/RepoUpdater.kt @@ -0,0 +1,101 @@ +package com.topjohnwu.magisk.core.tasks + +import com.topjohnwu.magisk.core.Const +import com.topjohnwu.magisk.core.model.module.Repo +import com.topjohnwu.magisk.data.database.RepoDao +import com.topjohnwu.magisk.data.network.GithubApiServices +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.rxkotlin.toFlowable +import io.reactivex.schedulers.Schedulers +import se.ansman.kotshi.JsonSerializable +import timber.log.Timber +import java.net.HttpURLConnection +import java.text.SimpleDateFormat +import java.util.* +import kotlin.collections.HashSet + +class RepoUpdater( + private val api: GithubApiServices, + private val repoDB: RepoDao +) { + private fun loadRepos(repos: List, cached: MutableSet) = + repos.toFlowable().parallel().runOn(Schedulers.io()).map { + // Skip submission + if (it.id == "submission") + return@map + val repo = repoDB.getRepo(it.id)?.apply { cached.remove(it.id) } ?: Repo(it.id) + repo.runCatching { + update(it.pushDate) + repoDB.addRepo(this) + }.getOrElse(Timber::e) + }.sequential() + + private fun loadPage( + cached: MutableSet, + page: Int = 1, + etag: String = "" + ): Flowable = api.fetchRepos(page, etag).flatMap { + it.error()?.also { throw it } + it.response()?.run { + if (code() == HttpURLConnection.HTTP_NOT_MODIFIED) + return@run Flowable.error(CachedException()) + + if (page == 1) + repoDB.etagKey = headers()[Const.Key.ETAG_KEY].orEmpty().trimEtag() + + val flow = loadRepos(body()!!, cached) + if (headers()[Const.Key.LINK_KEY].orEmpty().contains("next")) { + flow.mergeWith(loadPage(cached, page + 1)) + } else { + flow + } + } + } + + private fun forcedReload(cached: MutableSet) = + cached.toFlowable().parallel().runOn(Schedulers.io()).map { + runCatching { + Repo(it).update() + }.getOrElse(Timber::e) + }.sequential() + + private fun String.trimEtag() = substring(indexOf('\"'), lastIndexOf('\"') + 1) + + @Suppress("RedundantLambdaArrow") + operator fun invoke(forced: Boolean) : Completable { + return Flowable + .fromCallable { Collections.synchronizedSet(HashSet(repoDB.repoIDList)) } + .flatMap { cached -> + loadPage(cached, etag = repoDB.etagKey).doOnComplete { + repoDB.removeRepos(cached) + }.onErrorResumeNext { it: Throwable -> + if (it is CachedException) { + if (forced) + return@onErrorResumeNext forcedReload(cached) + } else { + Timber.e(it) + } + Flowable.empty() + } + }.ignoreElements() + } + + class CachedException : Exception() +} + +private val dateFormat: SimpleDateFormat = + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + +@JsonSerializable +data class GithubRepoInfo( + val name: String, + val pushed_at: String +) { + val id get() = name + + @Transient + val pushDate = dateFormat.parse(pushed_at)!! +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/BiometricHelper.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/BiometricHelper.kt similarity index 95% rename from app/src/main/java/com/topjohnwu/magisk/utils/BiometricHelper.kt rename to app/src/main/java/com/topjohnwu/magisk/core/utils/BiometricHelper.kt index 0b2bae237..523d7319f 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/BiometricHelper.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/BiometricHelper.kt @@ -1,11 +1,11 @@ -package com.topjohnwu.magisk.utils +package com.topjohnwu.magisk.core.utils import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity -import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.core.Config import org.koin.core.KoinComponent import org.koin.core.get diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/Keygen.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/Keygen.kt similarity index 85% rename from app/src/main/java/com/topjohnwu/magisk/utils/Keygen.kt rename to app/src/main/java/com/topjohnwu/magisk/core/utils/Keygen.kt index d1ffcf04a..993d1fc06 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/Keygen.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/Keygen.kt @@ -1,11 +1,11 @@ -package com.topjohnwu.magisk.utils +package com.topjohnwu.magisk.core.utils import android.content.pm.PackageManager import android.util.Base64 import android.util.Base64OutputStream -import com.topjohnwu.magisk.Config +import com.topjohnwu.magisk.core.Config +import com.topjohnwu.magisk.core.utils.PatchAPK.ALPHANUM import com.topjohnwu.magisk.di.koinModules -import com.topjohnwu.magisk.utils.PatchAPK.ALPHANUM import com.topjohnwu.signing.CryptoUtils.readCertificate import com.topjohnwu.signing.CryptoUtils.readPrivateKey import com.topjohnwu.superuser.internal.InternalUtils @@ -50,10 +50,14 @@ class Keygen: CertKeyProvider { private val provider: CertKeyProvider - inner class KeyStoreProvider : CertKeyProvider { + inner class KeyStoreProvider : + CertKeyProvider { private val ks by lazy { init() } override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate } - override val key by lazy { ks.getKey(ALIAS, PASSWORD) as PrivateKey } + override val key by lazy { ks.getKey( + ALIAS, + PASSWORD + ) as PrivateKey } } class TestProvider : CertKeyProvider { @@ -113,8 +117,12 @@ class Keygen: CertKeyProvider { if (raw.isEmpty()) { ks.load(null) } else { - GZIPInputStream(Base64.decode(raw, BASE64_FLAG).inputStream()).use { - ks.load(it, PASSWORD) + GZIPInputStream(Base64.decode(raw, + BASE64_FLAG + ).inputStream()).use { + ks.load(it, + PASSWORD + ) } } @@ -131,10 +139,16 @@ class Keygen: CertKeyProvider { val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer)) // Store them into keystore - ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert)) + ks.setKeyEntry( + ALIAS, kp.private, + PASSWORD, arrayOf(cert)) val bytes = ByteArrayOutputStream() - GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use { - ks.store(it, PASSWORD) + GZIPOutputStream(Base64OutputStream(bytes, + BASE64_FLAG + )).use { + ks.store(it, + PASSWORD + ) } Config.keyStoreRaw = bytes.toString("UTF-8") diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/Locales.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/Locales.kt similarity index 95% rename from app/src/main/java/com/topjohnwu/magisk/utils/Locales.kt rename to app/src/main/java/com/topjohnwu/magisk/core/utils/Locales.kt index f08bdb3d3..6cce37c15 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/Locales.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/Locales.kt @@ -1,13 +1,13 @@ @file:Suppress("DEPRECATION") -package com.topjohnwu.magisk.utils +package com.topjohnwu.magisk.core.utils import android.annotation.SuppressLint import android.content.res.Configuration import android.content.res.Resources -import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.ResourceMgr +import com.topjohnwu.magisk.core.Config +import com.topjohnwu.magisk.core.ResourceMgr import com.topjohnwu.magisk.extensions.langTagToLocale import com.topjohnwu.magisk.extensions.toLangTag import io.reactivex.Single diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt similarity index 94% rename from app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.kt rename to app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt index b1d4c00d3..8d3f9a984 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt @@ -1,15 +1,20 @@ -package com.topjohnwu.magisk.utils +package com.topjohnwu.magisk.core.utils import android.content.Context import android.os.Build.VERSION.SDK_INT import android.widget.Toast -import com.topjohnwu.magisk.* +import com.topjohnwu.magisk.BuildConfig +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.core.Config +import com.topjohnwu.magisk.core.Const +import com.topjohnwu.magisk.core.Info +import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.extensions.DynamicClassLoader import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.extensions.writeTo -import com.topjohnwu.magisk.view.Notifications +import com.topjohnwu.magisk.core.view.Notifications import com.topjohnwu.signing.JarMap import com.topjohnwu.signing.SignAPK import com.topjohnwu.superuser.Shell diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/ProgressInputStream.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/ProgressInputStream.kt similarity index 95% rename from app/src/main/java/com/topjohnwu/magisk/utils/ProgressInputStream.kt rename to app/src/main/java/com/topjohnwu/magisk/core/utils/ProgressInputStream.kt index 817f06566..ac0f26cb9 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/ProgressInputStream.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/ProgressInputStream.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.utils +package com.topjohnwu.magisk.core.utils import com.topjohnwu.superuser.internal.UiThreadHandler import java.io.FilterInputStream @@ -41,4 +41,4 @@ class ProgressInputStream( } return sz } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/RootInit.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/RootInit.kt similarity index 88% rename from app/src/main/java/com/topjohnwu/magisk/utils/RootInit.kt rename to app/src/main/java/com/topjohnwu/magisk/core/utils/RootInit.kt index 92d5a2729..5f75e1819 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/RootInit.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/RootInit.kt @@ -1,10 +1,10 @@ -package com.topjohnwu.magisk.utils +package com.topjohnwu.magisk.core.utils import android.content.Context -import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.core.Const +import com.topjohnwu.magisk.core.wrap import com.topjohnwu.magisk.extensions.rawResource -import com.topjohnwu.magisk.wrap import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.io.SuFile diff --git a/app/src/main/java/com/topjohnwu/magisk/core/utils/SafetyNetHelper.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/SafetyNetHelper.kt new file mode 100644 index 000000000..c27fc1836 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/SafetyNetHelper.kt @@ -0,0 +1,21 @@ +package com.topjohnwu.magisk.core.utils + +interface SafetyNetHelper { + + val version: Int + + fun attest() + + interface Callback { + fun onResponse(responseCode: Int) + } + + companion object { + + const val RESPONSE_ERR = 0x01 + const val CONNECTION_FAIL = 0x02 + + const val BASIC_PASS = 0x10 + const val CTS_PASS = 0x20 + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/Utils.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/Utils.kt similarity index 91% rename from app/src/main/java/com/topjohnwu/magisk/utils/Utils.kt rename to app/src/main/java/com/topjohnwu/magisk/core/utils/Utils.kt index 810b9eba6..1ab7849e7 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/Utils.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/Utils.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.utils +package com.topjohnwu.magisk.core.utils import android.content.Context import android.content.Intent @@ -7,10 +7,10 @@ import android.net.Uri import android.os.Environment import android.widget.Toast import androidx.work.* -import com.topjohnwu.magisk.* +import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.core.* import com.topjohnwu.magisk.extensions.get -import com.topjohnwu.magisk.model.update.UpdateCheckService import com.topjohnwu.superuser.internal.UiThreadHandler import java.io.File import java.util.concurrent.TimeUnit @@ -62,7 +62,10 @@ object Utils { if (intent.resolveActivity(context.packageManager) != null) { context.startActivity(intent) } else { - toast(R.string.open_link_failed_toast, Toast.LENGTH_SHORT) + toast( + R.string.open_link_failed_toast, + Toast.LENGTH_SHORT + ) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/ZipUtils.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/ZipUtils.kt similarity index 97% rename from app/src/main/java/com/topjohnwu/magisk/utils/ZipUtils.kt rename to app/src/main/java/com/topjohnwu/magisk/core/utils/ZipUtils.kt index 322750717..1267f19b7 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/ZipUtils.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/ZipUtils.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.utils +package com.topjohnwu.magisk.core.utils import com.topjohnwu.superuser.io.SuFile import com.topjohnwu.superuser.io.SuFileOutputStream diff --git a/app/src/main/java/com/topjohnwu/magisk/view/Notifications.kt b/app/src/main/java/com/topjohnwu/magisk/core/view/Notifications.kt similarity index 89% rename from app/src/main/java/com/topjohnwu/magisk/view/Notifications.kt rename to app/src/main/java/com/topjohnwu/magisk/core/view/Notifications.kt index d056a2e83..ddcc7765e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/Notifications.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/view/Notifications.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.view +package com.topjohnwu.magisk.core.view import android.app.Notification import android.app.NotificationChannel @@ -9,13 +9,12 @@ import android.os.Build.VERSION.SDK_INT import androidx.core.app.TaskStackBuilder import androidx.core.content.getSystemService import androidx.core.graphics.drawable.toIcon -import com.topjohnwu.magisk.* -import com.topjohnwu.magisk.Const.ID.PROGRESS_NOTIFICATION_CHANNEL -import com.topjohnwu.magisk.Const.ID.UPDATE_NOTIFICATION_CHANNEL +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.core.* +import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL +import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.getBitmap -import com.topjohnwu.magisk.model.receiver.GeneralReceiver -import com.topjohnwu.magisk.ui.SplashActivity object Notifications { @@ -52,10 +51,13 @@ object Notifications { val stackBuilder = TaskStackBuilder.create(context) stackBuilder.addParentStack(SplashActivity::class.java.cmp(context.packageName)) stackBuilder.addNextIntent(intent) - val pendingIntent = stackBuilder.getPendingIntent(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID, + val pendingIntent = stackBuilder.getPendingIntent( + Const.ID.MAGISK_UPDATE_NOTIFICATION_ID, PendingIntent.FLAG_UPDATE_CURRENT) - val builder = updateBuilder(context) + val builder = updateBuilder( + context + ) .setContentTitle(context.getString(R.string.magisk_update_title)) .setContentText(context.getString(R.string.manager_download_install)) .setAutoCancel(true) @@ -72,7 +74,9 @@ object Notifications { val pendingIntent = PendingIntent.getBroadcast(context, Const.ID.APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT) - val builder = updateBuilder(context) + val builder = updateBuilder( + context + ) .setContentTitle(context.getString(R.string.manager_update_title)) .setContentText(context.getString(R.string.manager_download_install)) .setAutoCancel(true) @@ -87,7 +91,9 @@ object Notifications { val pendingIntent = PendingIntent.getBroadcast(context, Const.ID.DTBO_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT) - val builder = updateBuilder(context) + val builder = updateBuilder( + context + ) .setContentTitle(context.getString(R.string.dtbo_patched_title)) .setContentText(context.getString(R.string.dtbo_patched_reboot)) diff --git a/app/src/main/java/com/topjohnwu/magisk/view/Shortcuts.kt b/app/src/main/java/com/topjohnwu/magisk/core/view/Shortcuts.kt similarity index 78% rename from app/src/main/java/com/topjohnwu/magisk/view/Shortcuts.kt rename to app/src/main/java/com/topjohnwu/magisk/core/view/Shortcuts.kt index 829cb5e8c..f4d0a3c78 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/Shortcuts.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/view/Shortcuts.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.view +package com.topjohnwu.magisk.core.view import android.content.Context import android.content.Intent @@ -10,17 +10,18 @@ import androidx.annotation.RequiresApi import androidx.core.content.getSystemService import androidx.core.graphics.drawable.toAdaptiveIcon import androidx.core.graphics.drawable.toIcon -import com.topjohnwu.magisk.* +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.core.* +import com.topjohnwu.magisk.core.utils.Utils import com.topjohnwu.magisk.extensions.getBitmap -import com.topjohnwu.magisk.ui.SplashActivity -import com.topjohnwu.magisk.utils.Utils object Shortcuts { fun setup(context: Context) { if (Build.VERSION.SDK_INT >= 25) { val manager = context.getSystemService() - manager?.dynamicShortcuts = getShortCuts(context) + manager?.dynamicShortcuts = + getShortCuts(context) } } @@ -77,19 +78,6 @@ object Shortcuts { .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) ) .setIcon(getIcon(R.drawable.sc_extension)) - .setRank(3) - .build() - ) - shortCuts.add( - ShortcutInfo.Builder(context, "downloads") - .setShortLabel(context.getString(R.string.downloads)) - .setIntent( - Intent(intent) - .putExtra(Const.Key.OPEN_SECTION, "downloads") - .setAction(Intent.ACTION_VIEW) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) - ) - .setIcon(getIcon(R.drawable.sc_cloud_download)) .setRank(2) .build() ) diff --git a/app/src/main/java/com/topjohnwu/magisk/data/database/Repo.kt b/app/src/main/java/com/topjohnwu/magisk/data/database/Repo.kt new file mode 100644 index 000000000..c3ea56535 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/data/database/Repo.kt @@ -0,0 +1,67 @@ +@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 + fun searchRepos(query: String, offset: Int, limit: Int = LIMIT): List + + @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 + + @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 + +} + +@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 + + @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 + + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDao.kt b/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDao.kt index 21a10f488..05f5f53a4 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDao.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDao.kt @@ -1,13 +1,15 @@ package com.topjohnwu.magisk.data.database import androidx.room.* -import com.topjohnwu.magisk.Config -import com.topjohnwu.magisk.model.entity.module.Repo +import com.topjohnwu.magisk.core.Config +import com.topjohnwu.magisk.core.model.module.Repo @Database(version = 6, entities = [Repo::class, RepoEtag::class]) abstract class RepoDatabase : RoomDatabase() { abstract fun repoDao() : RepoDao + abstract fun repoByUpdatedDao(): RepoByUpdatedDao + abstract fun repoByNameDao(): RepoByNameDao } @Dao diff --git a/app/src/main/java/com/topjohnwu/magisk/data/network/GithubServices.kt b/app/src/main/java/com/topjohnwu/magisk/data/network/GithubServices.kt index 1a10b592f..c072648bb 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/network/GithubServices.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/network/GithubServices.kt @@ -1,8 +1,8 @@ package com.topjohnwu.magisk.data.network -import com.topjohnwu.magisk.Const -import com.topjohnwu.magisk.model.entity.UpdateInfo -import com.topjohnwu.magisk.tasks.GithubRepoInfo +import com.topjohnwu.magisk.core.Const +import com.topjohnwu.magisk.core.tasks.GithubRepoInfo +import com.topjohnwu.magisk.core.model.UpdateInfo import io.reactivex.Flowable import io.reactivex.Single import okhttp3.ResponseBody @@ -78,4 +78,4 @@ interface GithubApiServices { @Query("sort") sort: String = "pushed", @Query("per_page") count: Int = 100): Flowable>> -} \ No newline at end of file +} diff --git a/app/src/main/java/com/topjohnwu/magisk/data/repository/DBConfig.kt b/app/src/main/java/com/topjohnwu/magisk/data/repository/DBConfig.kt index 0634ee1e6..a33bdde4b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/repository/DBConfig.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/repository/DBConfig.kt @@ -1,7 +1,7 @@ package com.topjohnwu.magisk.data.repository -import com.topjohnwu.magisk.data.database.SettingsDao -import com.topjohnwu.magisk.data.database.StringDao +import com.topjohnwu.magisk.core.magiskdb.SettingsDao +import com.topjohnwu.magisk.core.magiskdb.StringDao import io.reactivex.schedulers.Schedulers import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty diff --git a/app/src/main/java/com/topjohnwu/magisk/data/repository/LogRepository.kt b/app/src/main/java/com/topjohnwu/magisk/data/repository/LogRepository.kt index 2f0a3eaba..86ce26a30 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/repository/LogRepository.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/repository/LogRepository.kt @@ -1,20 +1,18 @@ package com.topjohnwu.magisk.data.repository -import com.topjohnwu.magisk.Const +import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.data.database.SuLogDao import com.topjohnwu.magisk.model.entity.MagiskLog -import com.topjohnwu.magisk.model.entity.WrappedMagiskLog import com.topjohnwu.superuser.Shell import io.reactivex.Completable import io.reactivex.Single -import java.util.concurrent.TimeUnit class LogRepository( private val logDao: SuLogDao ) { - fun fetchLogs() = logDao.fetchAll().map { it.wrap() } + fun fetchLogs() = logDao.fetchAll() fun fetchMagiskLogs() = Single.fromCallable { Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").exec().out @@ -28,11 +26,4 @@ class LogRepository( fun insert(log: MagiskLog) = logDao.insert(log) - private fun List.wrap(): List { - val day = TimeUnit.DAYS.toMillis(1) - return groupBy { it.time / day } - .map { WrappedMagiskLog(it.key * day, it.value) } - } - - } diff --git a/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt b/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt index eb1d3c5d7..1a3a73c1a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt @@ -1,8 +1,8 @@ package com.topjohnwu.magisk.data.repository import android.content.pm.PackageManager -import com.topjohnwu.magisk.Config -import com.topjohnwu.magisk.Info +import com.topjohnwu.magisk.core.Info +import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.extensions.getLabel import com.topjohnwu.magisk.extensions.packageName @@ -24,7 +24,8 @@ class MagiskRepository( Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate() Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate() Config.Value.CANARY_DEBUG_CHANNEL -> apiRaw.fetchCanaryDebugUpdate() - Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(Config.customChannelUrl) + Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate( + Config.customChannelUrl) else -> throw IllegalArgumentException() }.flatMap { // If remote version is lower than current installed, try switching to beta diff --git a/app/src/main/java/com/topjohnwu/magisk/data/repository/StringRepository.kt b/app/src/main/java/com/topjohnwu/magisk/data/repository/StringRepository.kt index 9395a06ea..74806db6c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/repository/StringRepository.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/repository/StringRepository.kt @@ -1,7 +1,7 @@ package com.topjohnwu.magisk.data.repository +import com.topjohnwu.magisk.core.model.module.Repo import com.topjohnwu.magisk.data.network.GithubRawServices -import com.topjohnwu.magisk.model.entity.module.Repo class StringRepository( private val api: GithubRawServices @@ -12,4 +12,4 @@ class StringRepository( fun getMetadata(repo: Repo) = api.fetchModuleInfo(repo.id, "module.prop") fun getReadme(repo: Repo) = api.fetchModuleInfo(repo.id, "README.md") -} \ No newline at end of file +} diff --git a/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt index 437c69c3a..979e6c304 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt @@ -6,6 +6,7 @@ import android.app.Application import android.content.Context import android.os.Build import android.os.Bundle +import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.preference.PreferenceManager import com.topjohnwu.magisk.utils.RxBus import org.koin.core.qualifier.named @@ -23,6 +24,7 @@ val applicationModule = module { single { PreferenceManager.getDefaultSharedPreferences(get(Protected)) } single { ActivityTracker() } factory { get().foreground ?: NullActivity } + single { LocalBroadcastManager.getInstance(get()) } } private fun createDEContext(context: Context): Context { diff --git a/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt index 6f1168687..0c1d1d9f1 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt @@ -2,8 +2,12 @@ package com.topjohnwu.magisk.di import android.content.Context import androidx.room.Room -import com.topjohnwu.magisk.data.database.* -import com.topjohnwu.magisk.tasks.RepoUpdater +import com.topjohnwu.magisk.core.magiskdb.PolicyDao +import com.topjohnwu.magisk.core.magiskdb.SettingsDao +import com.topjohnwu.magisk.core.magiskdb.StringDao +import com.topjohnwu.magisk.core.tasks.RepoUpdater +import com.topjohnwu.magisk.data.database.RepoDatabase +import com.topjohnwu.magisk.data.database.SuLogDatabase import org.koin.dsl.module @@ -11,7 +15,10 @@ val databaseModule = module { single { PolicyDao(get()) } single { SettingsDao() } single { StringDao() } - single { createRepoDatabase(get()).repoDao() } + single { createRepoDatabase(get()) } + single { get().repoDao() } + single { get().repoByNameDao() } + single { get().repoByUpdatedDao() } single { createSuLogDatabase(get(Protected)).suLogDao() } single { RepoUpdater(get(), get()) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt index 73db91cb9..7ae909000 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt @@ -4,7 +4,7 @@ import android.content.Context import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.topjohnwu.magisk.BuildConfig -import com.topjohnwu.magisk.Const +import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.data.network.GithubApiServices import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.net.Networking diff --git a/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt index b76c897f0..d85f332d5 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt @@ -1,25 +1,36 @@ package com.topjohnwu.magisk.di import android.net.Uri +import com.topjohnwu.magisk.legacy.flash.FlashViewModel +import com.topjohnwu.magisk.legacy.surequest.SuRequestViewModel import com.topjohnwu.magisk.ui.MainViewModel -import com.topjohnwu.magisk.ui.flash.FlashViewModel import com.topjohnwu.magisk.ui.hide.HideViewModel import com.topjohnwu.magisk.ui.home.HomeViewModel +import com.topjohnwu.magisk.ui.install.InstallViewModel import com.topjohnwu.magisk.ui.log.LogViewModel import com.topjohnwu.magisk.ui.module.ModuleViewModel +import com.topjohnwu.magisk.ui.request.RequestViewModel +import com.topjohnwu.magisk.ui.safetynet.SafetynetViewModel +import com.topjohnwu.magisk.ui.settings.SettingsViewModel import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel -import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel +import com.topjohnwu.magisk.ui.theme.ThemeViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module - val viewModelModules = module { - viewModel { MainViewModel() } + viewModel { HideViewModel(get()) } viewModel { HomeViewModel(get()) } - viewModel { SuperuserViewModel(get(), get(), get(), get()) } - viewModel { HideViewModel(get(), get()) } + viewModel { LogViewModel(get()) } viewModel { ModuleViewModel(get(), get(), get()) } - viewModel { LogViewModel(get(), get()) } + viewModel { RequestViewModel() } + viewModel { SafetynetViewModel(get()) } + viewModel { SettingsViewModel(get()) } + viewModel { SuperuserViewModel(get(), get(), get()) } + viewModel { ThemeViewModel() } + viewModel { InstallViewModel() } + viewModel { MainViewModel() } + + // Legacy viewModel { (action: String, file: Uri, additional: Uri) -> FlashViewModel(action, file, additional, get()) } diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt index 8b759fb00..0d0ef1839 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt @@ -52,9 +52,9 @@ fun Observable.subscribeK( fun Single.subscribeK( onError: OnErrorListener = { it.printStackTrace() }, - onSuccess: OnSuccessListener = {} + onNext: OnSuccessListener = {} ) = applySchedulers() - .subscribe(onSuccess, onError) + .subscribe(onNext, onError) fun Maybe.subscribeK( onError: OnErrorListener = { it.printStackTrace() }, @@ -198,5 +198,8 @@ fun ObservableField.toObservable(): Observable { fun T.toSingle() = Single.just(this) -fun zip(t1: Single, t2: Single, zipper: (T1, T2) -> R) = - Single.zip(t1, t2, BiFunction { rt1, rt2 -> zipper(rt1, rt2) }) +inline fun zip( + t1: Single, + t2: Single, + crossinline zipper: (T1, T2) -> R +) = Single.zip(t1, t2, BiFunction { rt1, rt2 -> zipper(rt1, rt2) }) \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/XAndroid.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/XAndroid.kt index 7d695bc59..995405bd1 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/XAndroid.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/XAndroid.kt @@ -28,14 +28,17 @@ import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.net.toFile import androidx.core.net.toUri -import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.FileProvider +import com.topjohnwu.magisk.core.Const +import com.topjohnwu.magisk.core.utils.currentLocale import com.topjohnwu.magisk.utils.DynamicClassLoader -import com.topjohnwu.magisk.utils.Utils -import com.topjohnwu.magisk.utils.currentLocale +import com.topjohnwu.magisk.core.utils.Utils import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.ShellUtils import java.io.File import java.io.FileNotFoundException +import java.text.SimpleDateFormat +import java.util.* import java.lang.reflect.Array as JArray val packageName: String get() = get().packageName @@ -283,7 +286,7 @@ fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.getDrawable(thi * with respect to RTL layout direction */ fun Context.startEndToLeftRight(start: Int, end: Int): Pair { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && + if (SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL ) { return end to start @@ -294,10 +297,10 @@ fun Context.startEndToLeftRight(start: Int, end: Int): Pair { fun Context.openUrl(url: String) = Utils.openLink(this, url.toUri()) @Suppress("FunctionName") -inline fun T.DynamicClassLoader(apk: File) - = DynamicClassLoader(apk, T::class.java.classLoader) +inline fun T.DynamicClassLoader(apk: File) = + DynamicClassLoader(apk, T::class.java.classLoader) -fun Context.unwrap() : Context { +fun Context.unwrap(): Context { var context = this while (true) { if (context is ContextWrapper) @@ -309,3 +312,38 @@ fun Context.unwrap() : Context { } fun Uri.writeTo(file: File) = toFile().copyTo(file) + +fun Context.hasPermissions(vararg permissions: String) = permissions.all { + ContextCompat.checkSelfPermission(this, it) == PERMISSION_GRANTED +} + +private val securityLevelFormatter get() = SimpleDateFormat("yyyy-MM-dd", + currentLocale +) + +/** Friendly reminder to seek newer roms or install oem updates. */ +val isDeviceSecure: Boolean + get() { + val latestPermittedTime = Calendar.getInstance().apply { + time = securityLevelDate + add(Calendar.MONTH, 2) + }.time.time + return now in 0..latestPermittedTime + } +val securityLevelDate get() = securityLevelFormatter.parseOrNull(securityLevel) ?: Date(0) +val securityLevel + get() = if (SDK_INT >= Build.VERSION_CODES.M) { + Build.VERSION.SECURITY_PATCH + } else { + null + } ?: "1970-01-01" //never + +val isSAR + get() = ShellUtils + .fastCmd("grep_prop ro.build.system_root_image") + .let { it.isNotEmpty() && it.toBoolean() } + +val isAB + get() = ShellUtils + .fastCmd("grep_prop ro.build.ab_update") + .let { it.isNotEmpty() && it.toBoolean() } diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/XJava.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/XJava.kt index a3aa1cf06..4a149528e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/XJava.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/XJava.kt @@ -1,11 +1,13 @@ package com.topjohnwu.magisk.extensions import android.os.Build +import timber.log.Timber import java.io.File import java.io.InputStream import java.io.OutputStream import java.lang.reflect.Field import java.lang.reflect.Method +import java.text.SimpleDateFormat import java.util.* import java.util.zip.ZipEntry import java.util.zip.ZipInputStream @@ -100,6 +102,9 @@ fun Locale.toLangTag(): String { } } +fun SimpleDateFormat.parseOrNull(date: String) = + runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull() + // Reflection hacks private val loadClass = ClassLoader::class.java.getMethod("loadClass", String::class.java) diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt index d60e9227a..fa93fa1bf 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt @@ -1,6 +1,6 @@ package com.topjohnwu.magisk.extensions -import com.topjohnwu.magisk.Info +import com.topjohnwu.magisk.core.Info import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.io.SuFileInputStream import com.topjohnwu.superuser.io.SuFileOutputStream @@ -11,4 +11,6 @@ fun reboot(reason: String = if (Info.recovery) "recovery" else "") { } fun File.suOutputStream() = SuFileOutputStream(this) -fun File.suInputStream() = SuFileInputStream(this) \ No newline at end of file +fun File.suInputStream() = SuFileInputStream(this) + +val hasRoot get() = Shell.rootAccess() diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/XString.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/XString.kt index ee2d279e2..4f817e035 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/XString.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/XString.kt @@ -2,7 +2,15 @@ package com.topjohnwu.magisk.extensions import android.content.res.Resources -val specialChars = arrayOf('!', '@', '#', '$', '%', '&', '?') +val specialChars = arrayOf('!', '@', '#', '$', '%', '&', '?') + +fun String.replaceRandomWithSpecial(passes: Int): String { + var string = this + repeat(passes) { + string = string.replaceRandomWithSpecial() + } + return string +} fun String.replaceRandomWithSpecial(): String { var random: Char diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/XTime.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/XTime.kt index c3fa76291..7b513cbed 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/XTime.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/XTime.kt @@ -1,6 +1,6 @@ package com.topjohnwu.magisk.extensions -import com.topjohnwu.magisk.utils.currentLocale +import com.topjohnwu.magisk.core.utils.currentLocale import java.text.DateFormat import java.text.ParseException import java.text.SimpleDateFormat @@ -14,7 +14,22 @@ fun String.toTime(format: DateFormat) = try { -1L } -val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", currentLocale) } -val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", currentLocale) } -val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, currentLocale) } -val timeFormatTime by lazy { SimpleDateFormat("h:mm a", currentLocale) } +val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", + currentLocale +) } +val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", + currentLocale +) } +val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, + currentLocale +) } +val timeFormatTime by lazy { SimpleDateFormat("h:mm a", + currentLocale +) } +val timeDateFormat by lazy { + DateFormat.getDateTimeInstance( + DateFormat.DEFAULT, + DateFormat.DEFAULT, + currentLocale + ) +} diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/XView.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/XView.kt index b34338d3d..16183dd61 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/XView.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/XView.kt @@ -1,7 +1,11 @@ package com.topjohnwu.magisk.extensions import android.view.View +import android.view.ViewGroup import android.view.ViewTreeObserver +import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import androidx.transition.AutoTransition +import androidx.transition.TransitionManager fun View.setOnViewReadyListener(callback: () -> Unit) = addOnGlobalLayoutListener(true, callback) @@ -11,4 +15,9 @@ fun View.addOnGlobalLayoutListener(oneShot: Boolean = false, callback: () -> Uni if (oneShot) viewTreeObserver.removeOnGlobalLayoutListener(this) callback() } - }) \ No newline at end of file + }) + +fun ViewGroup.startAnimations() { + val transition = AutoTransition().setInterpolator(FastOutSlowInInterpolator()).setDuration(400) + TransitionManager.beginDelayedTransition(this, transition) +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.kt b/app/src/main/java/com/topjohnwu/magisk/legacy/flash/FlashActivity.kt similarity index 93% rename from app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.kt rename to app/src/main/java/com/topjohnwu/magisk/legacy/flash/FlashActivity.kt index 73609318a..e41dadbaf 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/legacy/flash/FlashActivity.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.ui.flash +package com.topjohnwu.magisk.legacy.flash import android.content.Context import android.content.Intent @@ -6,22 +6,22 @@ import android.content.pm.ActivityInfo import android.net.Uri import android.os.Bundle import androidx.core.net.toUri -import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.base.BaseActivity +import com.topjohnwu.magisk.core.Const +import com.topjohnwu.magisk.core.intent import com.topjohnwu.magisk.databinding.ActivityFlashBinding import com.topjohnwu.magisk.extensions.snackbar -import com.topjohnwu.magisk.intent import com.topjohnwu.magisk.model.events.BackPressEvent import com.topjohnwu.magisk.model.events.PermissionEvent import com.topjohnwu.magisk.model.events.SnackbarEvent import com.topjohnwu.magisk.model.events.ViewEvent -import com.topjohnwu.magisk.view.Notifications +import com.topjohnwu.magisk.ui.base.BaseUIActivity +import com.topjohnwu.magisk.core.view.Notifications import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import java.io.File -open class FlashActivity : BaseActivity() { +open class FlashActivity : BaseUIActivity() { override val layoutRes: Int = R.layout.activity_flash override val themeRes: Int = R.style.MagiskTheme_Flashing diff --git a/app/src/main/java/com/topjohnwu/magisk/legacy/flash/FlashViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/legacy/flash/FlashViewModel.kt new file mode 100644 index 000000000..1c16b4751 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/legacy/flash/FlashViewModel.kt @@ -0,0 +1,110 @@ +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> { itemBinding, _, item -> + item.bind(itemBinding) + itemBinding.bindExtra(BR.viewModel, this@FlashViewModel) + } + + private val outItems = ObservableArrayList() + private val logItems = Collections.synchronizedList(mutableListOf()) + + 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() + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt b/app/src/main/java/com/topjohnwu/magisk/legacy/surequest/SuRequestActivity.kt similarity index 81% rename from app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt rename to app/src/main/java/com/topjohnwu/magisk/legacy/surequest/SuRequestActivity.kt index 2fdac91ab..337d26fe3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/legacy/surequest/SuRequestActivity.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.ui.surequest +package com.topjohnwu.magisk.legacy.surequest import android.content.Intent import android.content.pm.ActivityInfo @@ -6,16 +6,16 @@ import android.os.Build import android.os.Bundle import android.view.Window import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.base.BaseActivity +import com.topjohnwu.magisk.core.su.SuCallbackHandler +import com.topjohnwu.magisk.core.su.SuCallbackHandler.REQUEST import com.topjohnwu.magisk.databinding.ActivityRequestBinding import com.topjohnwu.magisk.model.events.DieEvent import com.topjohnwu.magisk.model.events.ViewActionEvent import com.topjohnwu.magisk.model.events.ViewEvent -import com.topjohnwu.magisk.utils.SuHandler -import com.topjohnwu.magisk.utils.SuHandler.REQUEST +import com.topjohnwu.magisk.ui.base.BaseUIActivity import org.koin.androidx.viewmodel.ext.android.viewModel -open class SuRequestActivity : BaseActivity() { +open class SuRequestActivity : BaseUIActivity() { override val layoutRes: Int = R.layout.activity_request override val themeRes: Int = R.style.MagiskTheme_SU @@ -36,7 +36,11 @@ open class SuRequestActivity : BaseActivity(null) + val title = KObservableField("") + val packageName = KObservableField("") + + val denyText = KObservableField(res.getString(R.string.deny)) + val warningText = KObservableField(res.getString(R.string.su_warning)) + + val selectedItemPosition = KObservableField(0) + + private val items = DiffObservableList(ComparableRvItem.callback) + private val itemBinding = ItemBinding.of> { binding, _, item -> + item.bind(binding) + } + + val adapter = BindingListViewAdapter>(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() + } + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/HideAppInfo.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/HideAppInfo.kt index 5d6b3f7dd..73d1410ea 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/HideAppInfo.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/HideAppInfo.kt @@ -14,3 +14,14 @@ class HideAppInfo( val processes = info.packageInfo?.processes?.distinct() ?: listOf(info.packageName) } + +data class StatefulProcess( + val name: String, + val packageName: String, + val isHidden: Boolean +) + +class ProcessHideApp( + val info: HideAppInfo, + val processes: List +) \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/MagiskLog.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/MagiskLog.kt index fd72d8869..6eea78783 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/MagiskLog.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/MagiskLog.kt @@ -3,10 +3,11 @@ package com.topjohnwu.magisk.model.entity import androidx.room.Entity import androidx.room.Ignore import androidx.room.PrimaryKey +import com.topjohnwu.magisk.core.model.MagiskPolicy +import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.ALLOW import com.topjohnwu.magisk.extensions.now import com.topjohnwu.magisk.extensions.timeFormatTime import com.topjohnwu.magisk.extensions.toTime -import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.ALLOW @Entity(tableName = "logs") data class MagiskLog( @@ -23,11 +24,6 @@ data class MagiskLog( @Ignore val timeString = time.toTime(timeFormatTime) } -data class WrappedMagiskLog( - val time: Long, - val items: List -) - fun MagiskPolicy.toLog( toUid: Int, fromPid: Int, diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt index 293d1064c..fb83bc3be 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt @@ -2,13 +2,13 @@ package com.topjohnwu.magisk.model.entity.internal import android.content.Context import android.os.Parcelable -import com.topjohnwu.magisk.Config -import com.topjohnwu.magisk.Info +import com.topjohnwu.magisk.core.Info +import com.topjohnwu.magisk.core.Config +import com.topjohnwu.magisk.core.model.module.Repo import com.topjohnwu.magisk.extensions.cachedFile import com.topjohnwu.magisk.extensions.get -import com.topjohnwu.magisk.model.entity.MagiskJson -import com.topjohnwu.magisk.model.entity.ManagerJson -import com.topjohnwu.magisk.model.entity.module.Repo +import com.topjohnwu.magisk.core.model.MagiskJson +import com.topjohnwu.magisk.core.model.ManagerJson import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.android.parcel.Parcelize import java.io.File @@ -59,7 +59,7 @@ sealed class DownloadSubject : Parcelable { val magisk: MagiskJson = Info.remote.magisk @Parcelize - protected data class Flash( + data class Flash( override val configuration: Configuration ) : Magisk() { override val url: String get() = magisk.link @@ -72,7 +72,7 @@ sealed class DownloadSubject : Parcelable { } @Parcelize - protected class Uninstall : Magisk() { + class Uninstall : Magisk() { override val configuration: Configuration get() = Configuration.Uninstall override val url: String get() = Info.remote.uninstaller.link @@ -83,7 +83,7 @@ sealed class DownloadSubject : Parcelable { } @Parcelize - protected class Download : Magisk() { + class Download : Magisk() { override val configuration: Configuration get() = Configuration.Download override val url: String get() = magisk.link @@ -103,4 +103,4 @@ sealed class DownloadSubject : Parcelable { } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ConsoleRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ConsoleRvItem.kt index e890f9f2d..d5d391945 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ConsoleRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ConsoleRvItem.kt @@ -6,7 +6,7 @@ import androidx.databinding.ViewDataBinding import androidx.recyclerview.widget.RecyclerView import com.topjohnwu.magisk.R -class ConsoleRvItem(val item: String) : LenientRvItem() { +open class ConsoleRvItem(val item: String) : LenientRvItem() { override val layoutRes: Int = R.layout.item_console override fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) { @@ -23,4 +23,8 @@ class ConsoleRvItem(val item: String) : LenientRvItem() { override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other) override fun itemSameAs(other: ConsoleRvItem) = item == other.item +} + +class ConsoleItem(item: String) : ConsoleRvItem(item) { + override val layoutRes = R.layout.item_console_md2 } \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HideRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HideRvItem.kt index bf7a0d2a3..c6be0412e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HideRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HideRvItem.kt @@ -1,92 +1,81 @@ package com.topjohnwu.magisk.model.entity.recycler +import android.view.View +import android.view.ViewGroup import com.topjohnwu.magisk.R import com.topjohnwu.magisk.databinding.ComparableRvItem import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback -import com.topjohnwu.magisk.extensions.inject +import com.topjohnwu.magisk.extensions.startAnimations import com.topjohnwu.magisk.extensions.toggle -import com.topjohnwu.magisk.model.entity.HideAppInfo -import com.topjohnwu.magisk.model.entity.HideTarget -import com.topjohnwu.magisk.model.entity.state.IndeterminateState -import com.topjohnwu.magisk.model.events.HideProcessEvent -import com.topjohnwu.magisk.utils.DiffObservableList +import com.topjohnwu.magisk.model.entity.ProcessHideApp +import com.topjohnwu.magisk.model.entity.StatefulProcess +import com.topjohnwu.magisk.model.observer.Observer +import com.topjohnwu.magisk.ui.hide.HideViewModel import com.topjohnwu.magisk.utils.KObservableField -import com.topjohnwu.magisk.utils.RxBus +import kotlin.math.roundToInt -class HideRvItem(val item: HideAppInfo, targets: List) : - ComparableRvItem() { +class HideItem(val item: ProcessHideApp) : ComparableRvItem() { - override val layoutRes: Int = R.layout.item_hide_app + override val layoutRes = R.layout.item_hide_md2 + + val packageName = item.info.info.packageName.orEmpty() + val items = item.processes.map { HideProcessItem(it) } - val packageName = item.info.packageName.orEmpty() - val items = DiffObservableList(callback).also { - val items = item.processes.map { - val isHidden = targets.any { target -> - packageName == target.packageName && it == target.process - } - HideProcessRvItem(packageName, it, isHidden) - } - it.update(items) - } - val isHiddenState = KObservableField(currentState) val isExpanded = KObservableField(false) + val itemsChecked = KObservableField(0) + val itemsCheckedPercent = Observer(itemsChecked) { + (itemsChecked.value.toFloat() / items.size * 100).roundToInt() + } - private val itemsProcess get() = items.filterIsInstance() - - private val currentState - get() = when (itemsProcess.count { it.isHidden.value }) { - items.size -> IndeterminateState.CHECKED - in 1 until items.size -> IndeterminateState.INDETERMINATE - else -> IndeterminateState.UNCHECKED - } + /** [toggle] depends on this functionality */ + private val isHidden get() = itemsChecked.value == items.size init { - itemsProcess.forEach { - it.isHidden.addOnPropertyChangedCallback { isHiddenState.value = currentState } - } + items.forEach { it.isHidden.addOnPropertyChangedCallback { recalculateChecked() } } + recalculateChecked() } - fun toggle() { - val desiredState = when (isHiddenState.value) { - IndeterminateState.INDETERMINATE, - IndeterminateState.UNCHECKED -> true - IndeterminateState.CHECKED -> false - } - itemsProcess.forEach { it.isHidden.value = desiredState } - isHiddenState.value = currentState + fun collapse(v: View) { + (v.parent.parent as? ViewGroup)?.startAnimations() + isExpanded.value = false } - fun toggleExpansion() { - if (items.size <= 1) return + fun toggle(v: View) { + (v.parent as? ViewGroup)?.startAnimations() isExpanded.toggle() } - override fun contentSameAs(other: HideRvItem): Boolean = items.all { other.items.contains(it) } - override fun itemSameAs(other: HideRvItem): Boolean = item.info == other.item.info + fun toggle(viewModel: HideViewModel): Boolean { + // contract implies that isHidden == all checked + if (!isHidden) { + items.filterNot { it.isHidden.value } + } else { + items + }.forEach { it.toggle(viewModel) } + return true + } + + private fun recalculateChecked() { + itemsChecked.value = items.count { it.isHidden.value } + } + + override fun contentSameAs(other: HideItem): Boolean = item == other.item + override fun itemSameAs(other: HideItem): Boolean = item.info == other.item.info } -class HideProcessRvItem( - val packageName: String, - val process: String, - isHidden: Boolean -) : ComparableRvItem() { +class HideProcessItem(val item: StatefulProcess) : ComparableRvItem() { - 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() - - init { - this.isHidden.addOnPropertyChangedCallback { - rxBus.post(HideProcessEvent(this@HideProcessRvItem)) - } + fun toggle(viewModel: HideViewModel) { + isHidden.toggle() + viewModel.toggleItem(this) } - fun toggle() = isHidden.toggle() + override fun contentSameAs(other: HideProcessItem) = item == other.item + override fun itemSameAs(other: HideProcessItem) = item.name == other.item.name - override fun contentSameAs(other: HideProcessRvItem): Boolean = itemSameAs(other) - override fun itemSameAs(other: HideProcessRvItem): Boolean = - packageName == other.packageName && process == other.process -} \ No newline at end of file +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HomeItems.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HomeItems.kt new file mode 100644 index 000000000..1281eb0f6 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HomeItems.kt @@ -0,0 +1,116 @@ +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() { + + 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() { + + abstract val items: List + 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 + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt index bb68251e7..fe919e39a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt @@ -1,73 +1,39 @@ package com.topjohnwu.magisk.model.entity.recycler +import androidx.databinding.Bindable +import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.databinding.ComparableRvItem -import com.topjohnwu.magisk.extensions.timeFormatMedium +import com.topjohnwu.magisk.extensions.timeDateFormat import com.topjohnwu.magisk.extensions.toTime -import com.topjohnwu.magisk.extensions.toggle import com.topjohnwu.magisk.model.entity.MagiskLog -import com.topjohnwu.magisk.model.entity.WrappedMagiskLog -import com.topjohnwu.magisk.utils.DiffObservableList -import com.topjohnwu.magisk.utils.KObservableField -class LogRvItem : ComparableRvItem() { - override val layoutRes: Int = R.layout.item_page_log +class LogItem(val item: MagiskLog) : ObservableItem() { - val items = DiffObservableList(callback) + override val layoutRes = R.layout.item_log_access_md2 - fun update(list: List) { - list.firstOrNull()?.isExpanded?.value = true - items.update(list) - } + val date = item.time.toTime(timeDateFormat) + var isTop = false + @Bindable get + set(value) { + field = value + notifyChange(BR.top) + } + var isBottom = false + @Bindable get + set(value) { + field = value + notifyChange(BR.bottom) + } - //two of these will never be present, safe to assume it's unique - override fun contentSameAs(other: LogRvItem): Boolean = false + override fun itemSameAs(other: LogItem) = item.appName == other.item.appName - override fun itemSameAs(other: LogRvItem): Boolean = false -} - -class LogItemRvItem( - item: WrappedMagiskLog -) : ComparableRvItem() { - override val layoutRes: Int = R.layout.item_superuser_log - - val date = item.time.toTime(timeFormatMedium) - val items: List> = 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() { - 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() { - override val layoutRes: Int = R.layout.item_page_magisk_log - - val items = DiffObservableList(callback) - - fun update(list: List) { - items.update(list) - } - - //two of these will never be present, safe to assume it's unique - override fun contentSameAs(other: MagiskLogRvItem): Boolean = false - - override fun itemSameAs(other: MagiskLogRvItem): Boolean = false + override fun contentSameAs(other: LogItem) = item.fromUid == other.item.fromUid && + item.toUid == other.item.toUid && + item.fromPid == other.item.fromPid && + item.packageName == other.item.packageName && + item.command == other.item.command && + item.action == other.item.action && + item.time == other.item.time && + isTop == other.isTop && + isBottom == other.isBottom } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt index 1af52be7b..c47dc3ae0 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt @@ -1,75 +1,162 @@ package com.topjohnwu.magisk.model.entity.recycler -import android.content.res.Resources -import androidx.annotation.StringRes +import androidx.databinding.Bindable +import androidx.databinding.Observable +import androidx.databinding.PropertyChangeRegistry +import androidx.databinding.ViewDataBinding +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.core.model.module.Module +import com.topjohnwu.magisk.core.model.module.Repo import com.topjohnwu.magisk.databinding.ComparableRvItem -import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback -import com.topjohnwu.magisk.extensions.get -import com.topjohnwu.magisk.extensions.toggle -import com.topjohnwu.magisk.model.entity.module.Module -import com.topjohnwu.magisk.model.entity.module.Repo +import com.topjohnwu.magisk.ui.module.ModuleViewModel import com.topjohnwu.magisk.utils.KObservableField -class ModuleRvItem(val item: Module) : ComparableRvItem() { +object SafeModeNotice : ComparableRvItem() { + 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 + } - val lastActionNotice = KObservableField("") - val isChecked = KObservableField(item.enable) - val isDeletable = KObservableField(item.remove) + override fun contentSameAs(other: SafeModeNotice) = this == other + override fun itemSameAs(other: SafeModeNotice) = this === other +} - init { - isChecked.addOnPropertyChangedCallback { - when (it) { - true -> { - item.enable = true - notice(R.string.disable_file_removed) - } - false -> { - item.enable = false - notice(R.string.disable_file_created) - } - } +object InstallModule : ComparableRvItem() { + override val layoutRes = R.layout.item_module_download + + override fun onBindingBound(binding: ViewDataBinding) { + super.onBindingBound(binding) + val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams + params?.isFullSpan = true + } + + override fun contentSameAs(other: InstallModule) = this == other + override fun itemSameAs(other: InstallModule) = this === other +} + +class SectionTitle( + val title: Int, + _button: Int = 0, + _icon: Int = 0 +) : ObservableItem() { + override val layoutRes = R.layout.item_section_md2 + + var button = _button + @Bindable get + set(value) { + field = value + notifyChange(BR.button) } - isDeletable.addOnPropertyChangedCallback { - when (it) { - true -> { - item.remove = true - notice(R.string.remove_file_created) - } - false -> { - item.remove = false - notice(R.string.remove_file_deleted) - } - } + var icon = _icon + @Bindable get + set(value) { + field = value + notifyChange(BR.icon) } - when { - item.updated -> notice(R.string.update_file_created) - item.remove -> notice(R.string.remove_file_created) + var hasButton = button != 0 || icon != 0 + @Bindable get + set(value) { + field = value + notifyChange(BR.hasButton) + } + + override fun onBindingBound(binding: ViewDataBinding) { + super.onBindingBound(binding) + val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams + params?.isFullSpan = true + } + + override fun itemSameAs(other: SectionTitle): Boolean = this === other + override fun contentSameAs(other: SectionTitle): Boolean = this === other +} + +sealed class RepoItem(val item: Repo) : ObservableItem() { + 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 } } - fun toggle() = isChecked.toggle() - fun toggleDelete() = isDeletable.toggle() + class Remote(item: Repo) : RepoItem(item) +} - private fun notice(@StringRes info: Int) { - lastActionNotice.value = get().getString(info) +class ModuleItem(val item: Module) : ObservableItem(), Observable { + + override val layoutRes = R.layout.item_module_md2 + + @get:Bindable + var repo: Repo? = null + set(value) { + field = value + notifyChange(BR.repo) + } + + @get:Bindable + var isEnabled = item.enable + set(value) { + field = value + item.enable = value + notifyChange(BR.enabled) + } + @get:Bindable + var isRemoved = item.remove + set(value) { + field = value + item.remove = value + notifyChange(BR.removed) + } + + val isUpdated get() = item.updated + val isModified get() = isRemoved || item.updated + + fun toggle() { + 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.description == other.item.description && item.name == other.item.name - override fun itemSameAs(other: ModuleRvItem): Boolean = item.id == other.item.id + override fun itemSameAs(other: ModuleItem): Boolean = item.id == other.item.id + } -class RepoRvItem(val item: Repo) : ComparableRvItem() { +abstract class ObservableItem : ComparableRvItem(), Observable { - override val layoutRes: Int = R.layout.item_repo + private val list = PropertyChangeRegistry() - override fun contentSameAs(other: RepoRvItem): Boolean = item == other.item + override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) { + list.remove(callback ?: return) + } - override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id -} \ No newline at end of file + override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) { + list.add(callback ?: return) + } + + fun notifyChange(id: Int) = list.notifyChange(this, id) + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/PolicyRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/PolicyRvItem.kt index f6610c363..1929549c4 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/PolicyRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/PolicyRvItem.kt @@ -2,51 +2,52 @@ package com.topjohnwu.magisk.model.entity.recycler import android.graphics.drawable.Drawable import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.core.model.MagiskPolicy import com.topjohnwu.magisk.databinding.ComparableRvItem -import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback -import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.magisk.extensions.toggle -import com.topjohnwu.magisk.model.entity.MagiskPolicy -import com.topjohnwu.magisk.model.events.PolicyEnableEvent import com.topjohnwu.magisk.model.events.PolicyUpdateEvent +import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel import com.topjohnwu.magisk.utils.KObservableField -import com.topjohnwu.magisk.utils.RxBus -class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem() { - - override val layoutRes: Int = R.layout.item_policy +class PolicyItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem() { + override val layoutRes = R.layout.item_policy_md2 val isExpanded = KObservableField(false) val isEnabled = KObservableField(item.policy == MagiskPolicy.ALLOW) val shouldNotify = KObservableField(item.notification) val shouldLog = KObservableField(item.logging) - fun toggle() = isExpanded.toggle() - - private val rxBus: RxBus by inject() - - private val currentStateItem + private val updatedPolicy get() = item.copy( policy = if (isEnabled.value) MagiskPolicy.ALLOW else MagiskPolicy.DENY, notification = shouldNotify.value, logging = shouldLog.value ) - init { - isEnabled.addOnPropertyChangedCallback { - it ?: return@addOnPropertyChangedCallback - rxBus.post(PolicyEnableEvent(this@PolicyRvItem, it)) - } - shouldNotify.addOnPropertyChangedCallback { - it ?: return@addOnPropertyChangedCallback - rxBus.post(PolicyUpdateEvent.Notification(currentStateItem)) - } - shouldLog.addOnPropertyChangedCallback { - it ?: return@addOnPropertyChangedCallback - rxBus.post(PolicyUpdateEvent.Log(currentStateItem)) + fun toggle(viewModel: SuperuserViewModel) { + if (isExpanded.value) { + toggle() + return } + isEnabled.toggle() + viewModel.togglePolicy(this, isEnabled.value) } - override fun contentSameAs(other: PolicyRvItem): Boolean = itemSameAs(other) - override fun itemSameAs(other: PolicyRvItem): Boolean = item.uid == other.item.uid -} \ No newline at end of file + fun toggle() { + isExpanded.toggle() + } + + fun toggleNotify(viewModel: SuperuserViewModel) { + shouldNotify.toggle() + viewModel.updatePolicy(PolicyUpdateEvent.Notification(updatedPolicy)) + } + + fun toggleLog(viewModel: SuperuserViewModel) { + shouldLog.toggle() + viewModel.updatePolicy(PolicyUpdateEvent.Log(updatedPolicy)) + } + + override fun contentSameAs(other: PolicyItem) = itemSameAs(other) + override fun itemSameAs(other: PolicyItem) = item.uid == other.item.uid + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SettingsItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SettingsItem.kt new file mode 100644 index 000000000..aa7858d6a --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SettingsItem.kt @@ -0,0 +1,191 @@ +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() { + + 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 bindable( + initialValue: T, + fieldId: Int, + crossinline setter: (T) -> Unit = {} + ) = object : ObservableProperty(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 : 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() { + + 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(), KoinComponent { + + override val layoutRes = R.layout.item_settings_input + open val showStrip = true + + protected val resources get() = get() + 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(), KoinComponent { + + override val layoutRes = R.layout.item_settings_selector + + protected val resources get() = get() + + abstract val entries: Array + abstract val entryValues: Array + + @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 + + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/TappableHeadlineItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/TappableHeadlineItem.kt new file mode 100644 index 000000000..178f3dbd0 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/TappableHeadlineItem.kt @@ -0,0 +1,44 @@ +package com.topjohnwu.magisk.model.entity.recycler + +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.databinding.ComparableRvItem + +sealed class TappableHeadlineItem : ComparableRvItem() { + + 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/TextItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/TextItem.kt new file mode 100644 index 000000000..1ce559ab8 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/TextItem.kt @@ -0,0 +1,19 @@ +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() { + 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ThemeItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ThemeItem.kt new file mode 100644 index 000000000..4997269e7 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ThemeItem.kt @@ -0,0 +1,14 @@ +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() { + + override val layoutRes = R.layout.item_theme + + override fun contentSameAs(other: ThemeItem) = itemSameAs(other) + override fun itemSameAs(other: ThemeItem) = theme == other.theme + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/EventExecutors.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/EventExecutors.kt new file mode 100644 index 000000000..8655aae47 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/EventExecutors.kt @@ -0,0 +1,23 @@ +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) + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/InstallExternalModuleEvent.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/InstallExternalModuleEvent.kt new file mode 100644 index 000000000..308ac927d --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/InstallExternalModuleEvent.kt @@ -0,0 +1,36 @@ +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() + intent.setData(data.data).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP) + context.startActivity(intent) + } + } + + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/OpenInappLinkEvent.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/OpenInappLinkEvent.kt new file mode 100644 index 000000000..6c02e267f --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/OpenInappLinkEvent.kt @@ -0,0 +1,31 @@ +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 diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/RxEvents.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/RxEvents.kt index a0dd317e4..fd9081a90 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/events/RxEvents.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/RxEvents.kt @@ -1,17 +1,11 @@ package com.topjohnwu.magisk.model.events -import com.topjohnwu.magisk.model.entity.MagiskPolicy -import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem -import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem -import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem +import com.topjohnwu.magisk.core.model.MagiskPolicy import com.topjohnwu.magisk.utils.RxBus -class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event - -class PolicyEnableEvent(val item: PolicyRvItem, val enable: Boolean) : RxBus.Event sealed class PolicyUpdateEvent(val item: MagiskPolicy) : RxBus.Event { class Notification(item: MagiskPolicy) : PolicyUpdateEvent(item) class Log(item: MagiskPolicy) : PolicyUpdateEvent(item) } -class ModuleUpdatedEvent(val item: ModuleRvItem) : RxBus.Event +data class SafetyNetResult(val responseCode: Int) : RxBus.Event diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/SnackbarEvent.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/SnackbarEvent.kt index c60d79835..73b57e1ee 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/events/SnackbarEvent.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/SnackbarEvent.kt @@ -3,13 +3,16 @@ package com.topjohnwu.magisk.model.events import android.content.Context import androidx.annotation.StringRes import com.google.android.material.snackbar.Snackbar +import com.topjohnwu.magisk.core.base.BaseActivity +import com.topjohnwu.magisk.extensions.snackbar +import com.topjohnwu.magisk.ui.base.BaseUIActivity class SnackbarEvent private constructor( @StringRes private val messageRes: Int, private val messageString: String?, val length: Int, val f: Snackbar.() -> Unit -) : ViewEvent() { +) : ViewEvent(), ActivityExecutor { constructor( @StringRes messageRes: Int, @@ -24,4 +27,11 @@ class SnackbarEvent private constructor( ) : this(-1, message, length, f) fun message(context: Context): String = messageString ?: context.getString(messageRes) + + override fun invoke(activity: BaseActivity) { + if (activity is BaseUIActivity<*, *>) { + activity.snackbar(activity.snackbarView, message(activity), length, f) + } + } + } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt index ef10c39c5..173746fcf 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt @@ -1,8 +1,28 @@ package com.topjohnwu.magisk.model.events -import com.topjohnwu.magisk.base.BaseActivity -import com.topjohnwu.magisk.model.entity.module.Repo +import android.app.Activity +import android.content.Context +import android.content.Intent +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.core.Const +import com.topjohnwu.magisk.core.base.BaseActivity +import com.topjohnwu.magisk.core.model.module.Repo +import com.topjohnwu.magisk.data.repository.MagiskRepository +import com.topjohnwu.magisk.extensions.DynamicClassLoader +import com.topjohnwu.magisk.extensions.subscribeK +import com.topjohnwu.magisk.extensions.writeTo +import com.topjohnwu.magisk.utils.RxBus +import com.topjohnwu.magisk.core.utils.SafetyNetHelper +import com.topjohnwu.magisk.view.MagiskDialog +import com.topjohnwu.magisk.view.MarkDownWindow +import com.topjohnwu.superuser.Shell +import dalvik.system.DexFile +import io.reactivex.Completable import io.reactivex.subjects.PublishSubject +import org.koin.core.KoinComponent +import org.koin.core.inject +import java.io.File +import java.lang.reflect.InvocationHandler /** * Class for passing events from ViewModels to Activities/Fragments @@ -15,33 +35,147 @@ abstract class ViewEvent { var handled = false } -data class OpenLinkEvent(val url: String) : ViewEvent() +class UpdateSafetyNetEvent : ViewEvent(), ContextExecutor, KoinComponent, SafetyNetHelper.Callback { -class ManagerInstallEvent : ViewEvent() -class MagiskInstallEvent : ViewEvent() + private val magiskRepo by inject() + private val rxBus by inject() -class ManagerChangelogEvent : ViewEvent() -class MagiskChangelogEvent : ViewEvent() + private lateinit var EXT_APK: File + private lateinit var EXT_DEX: File -class UninstallEvent : ViewEvent() -class EnvFixEvent : ViewEvent() + override fun invoke(context: Context) { + val die = ::EXT_APK.isInitialized -class UpdateSafetyNetEvent : ViewEvent() + EXT_APK = File("${context.filesDir.parent}/snet", "snet.jar") + EXT_DEX = File(EXT_APK.parent, "snet.dex") -class ViewActionEvent(val action: BaseActivity<*, *>.() -> Unit) : ViewEvent() + Completable.fromAction { + val loader = DynamicClassLoader(EXT_APK) + val dex = DexFile.loadDex(EXT_APK.path, EXT_DEX.path, 0) -class OpenFilePickerEvent : ViewEvent() + // Scan through the dex and find our helper class + var helperClass: Class<*>? = null + for (className in dex.entries()) { + if (className.startsWith("x.")) { + val cls = loader.loadClass(className) + if (InvocationHandler::class.java.isAssignableFrom(cls)) { + helperClass = cls + break + } + } + } + helperClass ?: throw Exception() -class OpenChangelogEvent(val item: Repo) : ViewEvent() -class InstallModuleEvent(val item: Repo) : ViewEvent() + val helper = helperClass.getMethod( + "get", + Class::class.java, Context::class.java, Any::class.java + ) + .invoke(null, SafetyNetHelper::class.java, context, this) as SafetyNetHelper -class PageChangedEvent : ViewEvent() + if (helper.version < Const.SNET_EXT_VER) + throw Exception() + + helper.attest() + }.subscribeK(onError = { + if (die) { + rxBus.post(SafetyNetResult(-1)) + } else { + Shell.sh("rm -rf " + EXT_APK.parent).exec() + EXT_APK.parentFile?.mkdir() + download(context, true) + } + }) + } + + @Suppress("SameParameterValue") + private fun download(context: Context, askUser: Boolean) { + fun downloadInternal() = magiskRepo.fetchSafetynet() + .map { it.byteStream().writeTo(EXT_APK) } + .subscribeK { invoke(context) } + + if (!askUser) { + downloadInternal() + return + } + + MagiskDialog(context) + .applyTitle(R.string.proprietary_title) + .applyMessage(R.string.proprietary_notice) + .cancellable(false) + .applyButton(MagiskDialog.ButtonType.POSITIVE) { + titleRes = R.string.yes + onClick { downloadInternal() } + } + .applyButton(MagiskDialog.ButtonType.NEGATIVE) { + titleRes = android.R.string.no + onClick { rxBus.post(SafetyNetResult(-2)) } + } + .reveal() + } + + override fun onResponse(responseCode: Int) { + rxBus.post(SafetyNetResult(responseCode)) + } +} + +class ViewActionEvent(val action: BaseActivity.() -> Unit) : ViewEvent(), ActivityExecutor { + override fun invoke(activity: BaseActivity) = activity.run(action) +} + +class OpenChangelogEvent(val item: Repo) : ViewEvent(), ContextExecutor { + override fun invoke(context: Context) { + MarkDownWindow.show(context, null, item.readme) + } +} class PermissionEvent( val permissions: List, val callback: PublishSubject -) : 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 DieEvent : ViewEvent() +class BackPressEvent : ViewEvent(), ActivityExecutor { + override fun invoke(activity: BaseActivity) { + activity.onBackPressed() + } +} + +class DieEvent : ViewEvent(), ActivityExecutor { + override fun invoke(activity: BaseActivity) { + activity.finish() + } +} + +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 + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/BiometricDialog.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/BiometricDialog.kt new file mode 100644 index 000000000..db74ef997 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/BiometricDialog.kt @@ -0,0 +1,38 @@ +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 + } + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/DarkThemeDialog.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/DarkThemeDialog.kt new file mode 100644 index 000000000..4244fdac2 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/DarkThemeDialog.kt @@ -0,0 +1,50 @@ +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? = 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() + } + + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/DialogEvent.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/DialogEvent.kt new file mode 100644 index 000000000..60355c914 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/DialogEvent.kt @@ -0,0 +1,20 @@ +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 diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/EnvFixDialog.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/EnvFixDialog.kt new file mode 100644 index 000000000..8571b6518 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/EnvFixDialog.kt @@ -0,0 +1,55 @@ +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() + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/ManagerInstallDialog.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/ManagerInstallDialog.kt new file mode 100644 index 000000000..f1822773a --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/ManagerInstallDialog.kt @@ -0,0 +1,36 @@ +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) } + } + } + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/ModuleInstallDialog.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/ModuleInstallDialog.kt new file mode 100644 index 000000000..f27499251 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/ModuleInstallDialog.kt @@ -0,0 +1,37 @@ +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() + } + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/SuperuserRevokeDialog.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/SuperuserRevokeDialog.kt new file mode 100644 index 000000000..39e24d36a --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/SuperuserRevokeDialog.kt @@ -0,0 +1,33 @@ +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 diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/UninstallDialog.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/UninstallDialog.kt new file mode 100644 index 000000000..bdeb0b838 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/UninstallDialog.kt @@ -0,0 +1,55 @@ +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) + } + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt b/app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt index 046ae6906..21e4672d6 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt @@ -3,8 +3,8 @@ package com.topjohnwu.magisk.model.flash import android.content.Context import android.net.Uri import androidx.core.os.postDelayed +import com.topjohnwu.magisk.core.tasks.FlashZip import com.topjohnwu.magisk.extensions.inject -import com.topjohnwu.magisk.tasks.FlashZip import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.internal.UiThreadHandler @@ -58,4 +58,4 @@ sealed class Flashing( } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt b/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt index cb63165f5..b3693706b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt @@ -1,7 +1,7 @@ package com.topjohnwu.magisk.model.flash import android.net.Uri -import com.topjohnwu.magisk.tasks.MagiskInstaller +import com.topjohnwu.magisk.core.tasks.MagiskInstaller import com.topjohnwu.superuser.Shell sealed class Patching( @@ -28,8 +28,7 @@ sealed class Patching( logs: MutableList, resultListener: FlashResultListener ) : Patching(file, console, logs, resultListener) { - override fun operations() = - extractZip() && handleFile(uri) && patchBoot() && storeBoot() + override fun operations() = doPatchFile(uri) } class SecondSlot( @@ -38,8 +37,7 @@ sealed class Patching( logs: MutableList, resultListener: FlashResultListener ) : Patching(file, console, logs, resultListener) { - override fun operations() = - findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA() + override fun operations() = secondSlot() } class Direct( @@ -48,8 +46,7 @@ sealed class Patching( logs: MutableList, resultListener: FlashResultListener ) : Patching(file, console, logs, resultListener) { - override fun operations() = - findImage() && extractZip() && patchBoot() && flashBoot() + override fun operations() = direct() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/navigation/MagiskNavigationEvent.kt b/app/src/main/java/com/topjohnwu/magisk/model/navigation/MagiskNavigationEvent.kt index fb11e1ad0..ff2be32e5 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/navigation/MagiskNavigationEvent.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/navigation/MagiskNavigationEvent.kt @@ -4,7 +4,10 @@ import android.os.Bundle import androidx.annotation.AnimRes import androidx.annotation.AnimatorRes import androidx.fragment.app.Fragment +import com.topjohnwu.magisk.core.base.BaseActivity +import com.topjohnwu.magisk.model.events.ActivityExecutor import com.topjohnwu.magisk.model.events.ViewEvent +import com.topjohnwu.magisk.ui.base.CompatActivity import kotlin.reflect.KClass @DslMarker @@ -14,12 +17,17 @@ class MagiskNavigationEvent( val navDirections: MagiskNavDirectionsBuilder, val navOptions: MagiskNavOptions, val animOptions: MagiskAnimBuilder -) : ViewEvent() { +) : ViewEvent(), ActivityExecutor { companion object { operator fun invoke(builder: Builder.() -> Unit) = Builder().apply(builder).build() } + override fun invoke(activity: BaseActivity) { + if (activity !is CompatActivity<*, *>) return + activity.navigation?.navigateTo(this) + } + @NavigationDslMarker class Builder { @@ -83,4 +91,4 @@ class MagiskAnimBuilder { var popExit = 0 val anySet: Boolean get() = enter != 0 || exit != 0 || popEnter != 0 || popExit != 0 -} \ No newline at end of file +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigation.kt b/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigation.kt index a08e4c15a..12ebe4d7e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigation.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigation.kt @@ -1,57 +1,105 @@ package com.topjohnwu.magisk.model.navigation -import com.topjohnwu.magisk.ui.hide.MagiskHideFragment +import android.content.Context +import android.content.Intent +import android.os.Build +import com.topjohnwu.magisk.core.Const +import com.topjohnwu.magisk.core.intent +import com.topjohnwu.magisk.ui.MainActivity +import com.topjohnwu.magisk.ui.hide.HideFragment import com.topjohnwu.magisk.ui.home.HomeFragment +import com.topjohnwu.magisk.ui.install.InstallFragment import com.topjohnwu.magisk.ui.log.LogFragment -import com.topjohnwu.magisk.ui.module.ModulesFragment -import com.topjohnwu.magisk.ui.module.ReposFragment +import com.topjohnwu.magisk.ui.module.ModuleFragment +import com.topjohnwu.magisk.ui.safetynet.SafetynetFragment import com.topjohnwu.magisk.ui.settings.SettingsFragment import com.topjohnwu.magisk.ui.superuser.SuperuserFragment - +import com.topjohnwu.magisk.ui.theme.ThemeFragment object Navigation { fun home() = MagiskNavigationEvent { - navDirections { destination = HomeFragment::class } - navOptions { popUpTo = HomeFragment::class } + navDirections { + destination = HomeFragment::class + } + navOptions { + popUpTo = HomeFragment::class + } } fun superuser() = MagiskNavigationEvent { - navDirections { destination = SuperuserFragment::class } + navDirections { + destination = SuperuserFragment::class + } } fun modules() = MagiskNavigationEvent { - navDirections { destination = ModulesFragment::class } - } - - fun repos() = MagiskNavigationEvent { - navDirections { destination = ReposFragment::class } + navDirections { + destination = ModuleFragment::class + } } fun hide() = MagiskNavigationEvent { - navDirections { destination = MagiskHideFragment::class } + navDirections { + destination = HideFragment::class + } + } + + fun safetynet() = MagiskNavigationEvent { + navDirections { destination = SafetynetFragment::class } } fun log() = MagiskNavigationEvent { - navDirections { destination = LogFragment::class } + navDirections { + destination = LogFragment::class + } } fun settings() = MagiskNavigationEvent { - navDirections { destination = SettingsFragment::class } + navDirections { + destination = SettingsFragment::class + } + } + + fun install() = MagiskNavigationEvent { + navDirections { destination = InstallFragment::class } + } + + fun theme() = MagiskNavigationEvent { + navDirections { destination = ThemeFragment::class } } fun fromSection(section: String) = when (section) { "superuser" -> superuser() "modules" -> modules() - "downloads" -> repos() "magiskhide" -> hide() "log" -> log() "settings" -> settings() else -> home() } + // redesign starts here + + fun start(launchIntent: Intent, context: Context) { + context.intent() + .putExtra( + Const.Key.OPEN_SECTION, launchIntent.getStringExtra( + Const.Key.OPEN_SECTION)) + .putExtra( + Const.Key.OPEN_SETTINGS, + launchIntent.action == ACTION_APPLICATION_PREFERENCES + ) + .also { context.startActivity(it) } + } object Main { const val OPEN_NAV = 1 } + + private val ACTION_APPLICATION_PREFERENCES + get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Intent.ACTION_APPLICATION_PREFERENCES + } else { + "cannot be null, cannot be empty" + } } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigator.kt b/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigator.kt index e2b755a9d..ff9dcfe26 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigator.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigator.kt @@ -8,6 +8,4 @@ interface Navigator { //TODO Elevate Fragment to MagiskFragment<*,*> once everything is on board with it val baseFragments: List> - fun navigateTo(event: MagiskNavigationEvent) - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/zip/Zip.kt b/app/src/main/java/com/topjohnwu/magisk/model/zip/Zip.kt deleted file mode 100644 index fc118b493..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/zip/Zip.kt +++ /dev/null @@ -1,75 +0,0 @@ -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) = - 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/tasks/RepoUpdater.kt b/app/src/main/java/com/topjohnwu/magisk/tasks/RepoUpdater.kt deleted file mode 100644 index c3ba9cee3..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/tasks/RepoUpdater.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.topjohnwu.magisk.tasks - -import com.topjohnwu.magisk.Const -import com.topjohnwu.magisk.data.database.RepoDao -import com.topjohnwu.magisk.data.network.GithubApiServices -import com.topjohnwu.magisk.model.entity.module.Repo -import io.reactivex.Flowable -import io.reactivex.Single -import io.reactivex.rxkotlin.toFlowable -import io.reactivex.schedulers.Schedulers -import se.ansman.kotshi.JsonSerializable -import timber.log.Timber -import java.net.HttpURLConnection -import java.text.SimpleDateFormat -import java.util.* -import kotlin.collections.HashSet - -class RepoUpdater( - private val api: GithubApiServices, - private val repoDB: RepoDao -) { - - private fun loadRepos(repos: List, cached: MutableSet) = - repos.toFlowable().parallel().runOn(Schedulers.io()).map { - // Skip submission - if (it.id == "submission") - return@map - (repoDB.getRepo(it.id)?.apply { cached.remove(it.id) } ?: - Repo(it.id)).runCatching { - update(it.pushDate) - repoDB.addRepo(this) - }.getOrElse { Timber.e(it) } - }.sequential() - - private fun loadPage( - cached: MutableSet, - page: Int = 1, - etag: String = "" - ): Flowable = api.fetchRepos(page, etag).flatMap { - it.error()?.also { throw it } - it.response()?.run { - if (code() == HttpURLConnection.HTTP_NOT_MODIFIED) - return@run Flowable.error(CachedException) - - if (page == 1) - repoDB.etagKey = headers()[Const.Key.ETAG_KEY].orEmpty().trimEtag() - - val flow = loadRepos(body()!!, cached) - if (headers()[Const.Key.LINK_KEY].orEmpty().contains("next")) { - flow.mergeWith(loadPage(cached, page + 1)) - } else { - flow - } - } - } - - private fun forcedReload(cached: MutableSet) = - cached.toFlowable().parallel().runOn(Schedulers.io()).map { - runCatching { - Repo(it).update() - }.getOrElse { Timber.e(it) } - }.sequential() - - private fun String.trimEtag() = substring(indexOf('\"'), lastIndexOf('\"') + 1) - - operator fun invoke(forced: Boolean = false) : Single { - val cached = Collections.synchronizedSet(HashSet(repoDB.repoIDList)) - return loadPage(cached, etag = repoDB.etagKey).doOnComplete { - repoDB.removeRepos(cached) - }.onErrorResumeNext { it: Throwable -> - if (it is CachedException) { - if (forced) - return@onErrorResumeNext forcedReload(cached) - } else { - Timber.e(it) - } - Flowable.empty() - }.ignoreElements().toSingleDefault(Unit) - } - - object CachedException : Exception() -} - -private val dateFormat: SimpleDateFormat = - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { - timeZone = TimeZone.getTimeZone("UTC") - } - -@JsonSerializable -data class GithubRepoInfo( - val name: String, - val pushed_at: String -) { - val id get() = name - - @Transient - val pushDate = dateFormat.parse(pushed_at)!! -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt index e1d83a2a5..6466b9aa0 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt @@ -1,252 +1,161 @@ package com.topjohnwu.magisk.ui -import android.content.Intent import android.os.Bundle -import androidx.appcompat.app.AlertDialog -import androidx.core.view.GravityCompat +import android.view.MenuItem +import android.view.View +import android.view.ViewTreeObserver +import android.view.WindowManager +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.graphics.Insets +import androidx.core.view.setPadding +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentTransaction +import com.google.android.material.card.MaterialCardView import com.ncapdevi.fragnav.FragNavController -import com.ncapdevi.fragnav.FragNavTransactionOptions -import com.topjohnwu.magisk.Const -import com.topjohnwu.magisk.Const.Key.OPEN_SECTION -import com.topjohnwu.magisk.Info import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.base.BaseActivity -import com.topjohnwu.magisk.base.BaseFragment -import com.topjohnwu.magisk.databinding.ActivityMainBinding -import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback -import com.topjohnwu.magisk.extensions.snackbar -import com.topjohnwu.magisk.intent -import com.topjohnwu.magisk.model.events.* -import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder -import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent +import com.topjohnwu.magisk.core.Const +import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding +import com.topjohnwu.magisk.extensions.startAnimations import com.topjohnwu.magisk.model.navigation.Navigation -import com.topjohnwu.magisk.model.navigation.Navigator -import com.topjohnwu.magisk.ui.hide.MagiskHideFragment +import com.topjohnwu.magisk.ui.base.CompatActivity +import com.topjohnwu.magisk.ui.base.CompatNavigationDelegate import com.topjohnwu.magisk.ui.home.HomeFragment -import com.topjohnwu.magisk.ui.log.LogFragment -import com.topjohnwu.magisk.ui.module.ModulesFragment -import com.topjohnwu.magisk.ui.module.ReposFragment -import com.topjohnwu.magisk.ui.settings.SettingsFragment +import com.topjohnwu.magisk.ui.module.ModuleFragment import com.topjohnwu.magisk.ui.superuser.SuperuserFragment -import com.topjohnwu.magisk.utils.Utils +import com.topjohnwu.magisk.utils.HideBottomViewOnScrollBehavior +import com.topjohnwu.magisk.utils.HideTopViewOnScrollBehavior +import com.topjohnwu.magisk.utils.HideableBehavior +import com.topjohnwu.superuser.Shell import org.koin.androidx.viewmodel.ext.android.viewModel -import timber.log.Timber import kotlin.reflect.KClass -open class MainActivity : BaseActivity(), Navigator, - FragNavController.RootFragmentListener, FragNavController.TransactionListener { +open class MainActivity : CompatActivity(), + FragNavController.TransactionListener { - override val layoutRes: Int = R.layout.activity_main - override val viewModel: MainViewModel by viewModel() - private val navHostId: Int = R.id.main_nav_host - private val defaultPosition: Int = 0 + override val layoutRes = R.layout.activity_main_md2 + override val viewModel by viewModel() + override val navHost: Int = R.id.main_nav_host - private val navigationController by lazy { - FragNavController(supportFragmentManager, navHostId) - } - private val isRootFragment get() = - navigationController.currentStackIndex != defaultPosition + override val navigation by lazy { CompatNavigationDelegate(this, this) } override val baseFragments: List> = listOf( HomeFragment::class, - SuperuserFragment::class, - MagiskHideFragment::class, - ModulesFragment::class, - ReposFragment::class, - LogFragment::class, - SettingsFragment::class + ModuleFragment::class, + SuperuserFragment::class ) - override fun onCreate(savedInstanceState: Bundle?) { - if (!SplashActivity.DONE) { - startActivity(intent()) - finish() - } + //This temporarily fixes unwanted feature of BottomNavigationView - where the view applies + //padding on itself given insets are not consumed beforehand. Unfortunately the listener + //implementation doesn't favor us against the design library, so on re-create it's often given + //upper hand. + private val navObserver = ViewTreeObserver.OnGlobalLayoutListener { + binding.mainNavigation.setPadding(0) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (Info.env.isUnsupported && !viewModel.shownUnsupportedDialog) { - viewModel.shownUnsupportedDialog = true - AlertDialog.Builder(this) - .setTitle(R.string.unsupport_magisk_title) - .setMessage(getString(R.string.unsupport_magisk_msg, Const.Version.MIN_VERSION)) - .setPositiveButton(android.R.string.ok, null) - .show() + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + + setSupportActionBar(binding.mainToolbar) + + binding.mainToolbarWrapper.updateLayoutParams { + behavior = HideTopViewOnScrollBehavior() + } + binding.mainBottomBar.updateLayoutParams { + behavior = HideBottomViewOnScrollBehavior() + } + binding.mainNavigation.setOnNavigationItemSelectedListener { + when (it.itemId) { + R.id.homeFragment -> Navigation.home() + R.id.modulesFragment -> Navigation.modules() + R.id.superuserFragment -> Navigation.superuser() + else -> throw NotImplementedError("Id ${it.itemId} is not defined as selectable") + }.dispatchOnSelf() + true + } + binding.mainNavigation.setOnNavigationItemReselectedListener { + navigation.onReselected() } - navigationController.apply { - rootFragmentListener = this@MainActivity - transactionListener = this@MainActivity - initialize(defaultPosition, savedInstanceState) + binding.mainNavigation.viewTreeObserver.addOnGlobalLayoutListener(navObserver) + + if (intent.getBooleanExtra(Const.Key.OPEN_SETTINGS, false)) { + Navigation.settings().dispatchOnSelf() } - checkHideSection() - setSupportActionBar(binding.mainInclude.mainToolbar) + if (savedInstanceState != null) { + onTabTransaction(null, -1) - viewModel.isConnected.addOnPropertyChangedCallback { - checkHideSection() - } - - if (savedInstanceState == null) { - intent.getStringExtra(OPEN_SECTION)?.let { - onEventDispatched(Navigation.fromSection(it)) + if (!navigation.isRoot) { + requestNavigationHidden() } } } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - navigationController.onSaveInstanceState(outState) - } - - override fun setTitle(title: CharSequence?) { - supportActionBar?.title = title - } - - override fun setTitle(titleId: Int) { - supportActionBar?.setTitle(titleId) - } - - override fun onBackPressed() { - if (binding.drawerLayout.isDrawerOpen(binding.navView)) { - binding.drawerLayout.closeDrawer(binding.navView) - } else { - val fragment = navigationController.currentFrag as? BaseFragment<*, *> - - if (fragment?.onBackPressed() == true) { - return - } - - try { - navigationController.popFragment() - } catch (e: UnsupportedOperationException) { - when { - isRootFragment -> { - val options = FragNavTransactionOptions.newBuilder() - .transition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE) - .build() - navigationController.switchTab(defaultPosition, options) - } - else -> super.onBackPressed() - } - } + override fun onResume() { + super.onResume() + binding.mainNavigation.menu.apply { + val isRoot = Shell.rootAccess() + findItem(R.id.modulesFragment)?.isEnabled = isRoot + findItem(R.id.superuserFragment)?.isEnabled = isRoot } } - override fun onEventDispatched(event: ViewEvent) { - super.onEventDispatched(event) - when (event) { - is SnackbarEvent -> snackbar(snackbarView, event.message(this), event.length, event.f) - is BackPressEvent -> onBackPressed() - is MagiskNavigationEvent -> navigateTo(event) - is ViewActionEvent -> event.action(this) - is PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) { - onSuccess { event.callback.onNext(true) } - onFailure { - event.callback.onNext(false) - event.callback.onError(SecurityException("User refused permissions")) - } - } - } + override fun onDestroy() { + binding.mainNavigation.viewTreeObserver.removeOnGlobalLayoutListener(navObserver) + super.onDestroy() } - override fun onSimpleEventDispatched(event: Int) { - super.onSimpleEventDispatched(event) - when (event) { - Navigation.Main.OPEN_NAV -> openNav() + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> onBackPressed() + else -> return super.onOptionsItemSelected(item) } + return true } - private fun openNav() = binding.drawerLayout.openDrawer(GravityCompat.START) - - private fun checkHideSection() { - val menu = binding.navView.menu - menu.findItem(R.id.magiskHideFragment).isVisible = Info.env.isActive && Info.env.magiskHide - menu.findItem(R.id.modulesFragment).isVisible = Info.env.isActive - menu.findItem(R.id.reposFragment).isVisible = Info.isConnected.value && Info.env.isActive - menu.findItem(R.id.logFragment).isVisible = Info.env.isActive - menu.findItem(R.id.superuserFragment).isVisible = Utils.showSuperUser() - } - - private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) = - customAnimations(options.enter, options.exit, options.popEnter, options.popExit).apply { - if (!options.anySet) { - transition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) - } - } - - override val numberOfRootFragments: Int get() = baseFragments.size - - override fun getRootFragment(index: Int) = baseFragments[index].java.newInstance() - - override fun onTabTransaction(fragment: Fragment?, index: Int) { - val fragmentId = when (fragment) { - is HomeFragment -> R.id.magiskFragment - is SuperuserFragment -> R.id.superuserFragment - is MagiskHideFragment -> R.id.magiskHideFragment - is ModulesFragment -> R.id.modulesFragment - is ReposFragment -> R.id.reposFragment - is LogFragment -> R.id.logFragment - is SettingsFragment -> R.id.settings - else -> return - } - binding.navView.setCheckedItem(fragmentId) - } - - override fun navigateTo(event: MagiskNavigationEvent) { - val directions = event.navDirections - - navigationController.defaultTransactionOptions = FragNavTransactionOptions.newBuilder() - .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 destination = event.navDirections.destination?.java ?: let { - Timber.e("Cannot navigate to null destination") - return - } - val options = event.navOptions - - Intent(this, destination) - .putExtras(event.navDirections.args) - .apply { - if (options.singleTop) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) - if (options.clearTask) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) - } - .let { startActivity(it) } - } - - private fun navigateToFragment(event: MagiskNavigationEvent) { - val destination = event.navDirections.destination?.java ?: let { - Timber.e("Cannot navigate to null destination") - return - } - - when (val index = baseFragments.indexOfFirst { it.java.name == destination.name }) { - -1 -> destination.newInstance() - .apply { arguments = event.navDirections.args } - .let { navigationController.pushFragment(it) } - // When it's desired that fragments of same class are put on top of one another edit this - else -> navigationController.switchTab(index) - } - } + override fun onTabTransaction(fragment: Fragment?, index: Int) = + onFragmentTransaction(fragment, FragNavController.TransactionType.PUSH) override fun onFragmentTransaction( - fragment: Fragment?, - transactionType: FragNavController.TransactionType - ) = Unit + fragment: Fragment?, + transactionType: FragNavController.TransactionType + ) { + setDisplayHomeAsUpEnabled(!navigation.isRoot) + requestNavigationHidden(!navigation.isRoot) + } + + override fun peekSystemWindowInsets(insets: Insets) { + viewModel.insets.value = insets + } + + private fun setDisplayHomeAsUpEnabled(isEnabled: Boolean) { + binding.mainToolbar.startAnimations() + when { + isEnabled -> binding.mainToolbar.setNavigationIcon(R.drawable.ic_back_md2) + else -> binding.mainToolbar.navigationIcon = null + } + } + + @Suppress("UNCHECKED_CAST") + internal fun requestNavigationHidden(hide: Boolean = true) { + val topView = binding.mainToolbarWrapper + val bottomView = binding.mainBottomBar + + val topParams = topView.layoutParams as? CoordinatorLayout.LayoutParams + val bottomParams = bottomView.layoutParams as? CoordinatorLayout.LayoutParams + + val topBehavior = topParams?.behavior as? HideableBehavior + val bottomBehavior = bottomParams?.behavior as? HideableBehavior + + topBehavior?.setHidden(topView, hide = false, lockState = false) + bottomBehavior?.setHidden(bottomView, hide, hide) + } + + fun invalidateToolbar() { + //binding.mainToolbar.startAnimations() + binding.mainToolbar.invalidate() + } + } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt index 8bb5bf5a1..5451829da 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt @@ -1,29 +1,5 @@ package com.topjohnwu.magisk.ui -import android.view.MenuItem -import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.base.viewmodel.BaseViewModel -import com.topjohnwu.magisk.model.navigation.Navigation +import com.topjohnwu.magisk.ui.base.BaseViewModel - -class MainViewModel : BaseViewModel() { - - var shownUnsupportedDialog = false - - fun navPressed() = Navigation.Main.OPEN_NAV.publish() - - fun navigationItemPressed(item: MenuItem): Boolean { - when (item.itemId) { - R.id.magiskFragment -> Navigation.home() - R.id.superuserFragment -> Navigation.superuser() - R.id.magiskHideFragment -> Navigation.hide() - R.id.modulesFragment -> Navigation.modules() - R.id.reposFragment -> Navigation.repos() - R.id.logFragment -> Navigation.log() - R.id.settings -> Navigation.settings() - else -> null - }?.publish()?.let { return@navigationItemPressed true } - return false - } - -} +class MainViewModel : BaseViewModel() diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/ReselectionTarget.kt b/app/src/main/java/com/topjohnwu/magisk/ui/ReselectionTarget.kt new file mode 100644 index 000000000..2134ecc1b --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/ReselectionTarget.kt @@ -0,0 +1,7 @@ +package com.topjohnwu.magisk.ui + +interface ReselectionTarget { + + fun onReselected() + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseUIActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseUIActivity.kt new file mode 100644 index 000000000..a5a2e877d --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseUIActivity.kt @@ -0,0 +1,39 @@ +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 : + 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(this, layoutRes).apply { + setVariable(BR.viewModel, viewModel) + lifecycleOwner = this@BaseUIActivity + } + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseUIFragment.kt similarity index 75% rename from app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt rename to app/src/main/java/com/topjohnwu/magisk/ui/base/BaseUIFragment.kt index 368bb4e91..a6992d0a7 100644 --- a/app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseUIFragment.kt @@ -1,25 +1,23 @@ -package com.topjohnwu.magisk.base +package com.topjohnwu.magisk.ui.base import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.annotation.CallSuper import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding import androidx.fragment.app.Fragment import com.topjohnwu.magisk.BR -import com.topjohnwu.magisk.base.viewmodel.BaseViewModel import com.topjohnwu.magisk.model.events.EventHandler import com.topjohnwu.magisk.model.events.ViewEvent -abstract class BaseFragment : +abstract class BaseUIFragment : Fragment(), EventHandler { - protected val activity get() = requireActivity() as BaseActivity<*, *> + protected val activity get() = requireActivity() as BaseUIActivity<*, *> protected lateinit var binding: Binding protected abstract val layoutRes: Int - protected abstract val viewModel: ViewModel + abstract val viewModel: ViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -33,13 +31,12 @@ abstract class BaseFragment(inflater, layoutRes, container, false).apply { setVariable(BR.viewModel, viewModel) - lifecycleOwner = this@BaseFragment + lifecycleOwner = this@BaseUIFragment } return binding.root } - @CallSuper override fun onEventDispatched(event: ViewEvent) { super.onEventDispatched(event) activity.onEventDispatched(event) diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseViewModel.kt new file mode 100644 index 000000000..3f3acf842 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseViewModel.kt @@ -0,0 +1,202 @@ +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 get() = _viewEvents + val insets = KObservableField(Insets.NONE) + + var state: State = initialState + set(value) { + field = value + notifyStateChanged() + } + + private val disposables = CompositeDisposable() + private val _viewEvents = MutableLiveData() + 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 { + val subject = PublishSubject.create() + return subject.doOnSubscribeUi { PermissionEvent(permissions.toList(), subject).publish() } + } + + fun back() = BackPressEvent().publish() + + fun 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 Observable.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 Single.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 Maybe.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 Flowable.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 +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatActivity.kt new file mode 100644 index 000000000..9e2e9e58c --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatActivity.kt @@ -0,0 +1,88 @@ +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 : + BaseUIActivity(), CompatView, Navigator { + + override val themeRes = Theme.selected.themeRes + override val viewRoot: View get() = binding.root + override val navigation: CompatNavigationDelegate>? by lazy { + CompatNavigationDelegate(this) + } + override val baseFragments = listOf>() + 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() { + 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() + ?.hideSoftInputFromWindow(view.windowToken, 0) + view.clearFocus() +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatDelegate.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatDelegate.kt new file mode 100644 index 000000000..081eed217 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatDelegate.kt @@ -0,0 +1,76 @@ +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 + ) + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatFragment.kt new file mode 100644 index 000000000..bdc1fcbfe --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatFragment.kt @@ -0,0 +1,57 @@ +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 + : BaseUIFragment(), CompatView { + + 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() { + 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() +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatHelpers.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatHelpers.kt new file mode 100644 index 000000000..62ed84dcd --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatHelpers.kt @@ -0,0 +1,47 @@ +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 > diffListOf( + vararg newItems: T +) = diffListOf(newItems.toList()) + +inline fun > diffListOf( + newItems: List +) = DiffObservableList(object : DiffObservableList.Callback { + 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 > filterableListOf( + vararg newItems: T +) = FilterableDiffObservableList(object : DiffObservableList.Callback { + 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 > adapterOf() = object : BindingRecyclerViewAdapter() { + 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 > itemBindingOf( + crossinline body: (ItemBinding<*>) -> Unit = {} +) = OnItemBind { itemBinding, _, item -> + item.bind(itemBinding) + body(itemBinding) +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatNavigationDelegate.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatNavigationDelegate.kt new file mode 100644 index 000000000..990b20615 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatNavigationDelegate.kt @@ -0,0 +1,128 @@ +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( + 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 + ) + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatView.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatView.kt new file mode 100644 index 000000000..c6d8fe5af --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatView.kt @@ -0,0 +1,15 @@ +package com.topjohnwu.magisk.ui.base + +import android.view.View +import androidx.core.graphics.Insets + +internal interface CompatView { + + val viewRoot: View + val viewModel: ViewModel + val navigation: CompatNavigationDelegate<*>? + + fun peekSystemWindowInsets(insets: Insets) = Unit + fun consumeSystemWindowInsets(insets: Insets): Insets? = null + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/Queryable.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/Queryable.kt new file mode 100644 index 000000000..c7dc2d267 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/Queryable.kt @@ -0,0 +1,23 @@ +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() {} + } + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashFragment.kt new file mode 100644 index 000000000..ccb0a3b14 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashFragment.kt @@ -0,0 +1,13 @@ +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() { + + override val layoutRes = R.layout.fragment_flash_md2 + override val viewModel by viewModel() + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt index e2ead2b3b..4f1b716ed 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt @@ -1,110 +1,5 @@ package com.topjohnwu.magisk.ui.flash -import android.Manifest.permission.READ_EXTERNAL_STORAGE -import android.Manifest.permission.WRITE_EXTERNAL_STORAGE -import android.content.res.Resources -import android.net.Uri -import android.os.Handler -import androidx.core.os.postDelayed -import androidx.databinding.ObservableArrayList -import com.topjohnwu.magisk.BR -import com.topjohnwu.magisk.Config -import com.topjohnwu.magisk.Const -import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.base.viewmodel.BaseViewModel -import com.topjohnwu.magisk.databinding.ComparableRvItem -import com.topjohnwu.magisk.extensions.* -import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem -import com.topjohnwu.magisk.model.events.SnackbarEvent -import com.topjohnwu.magisk.model.flash.FlashResultListener -import com.topjohnwu.magisk.model.flash.Flashing -import com.topjohnwu.magisk.model.flash.Patching -import com.topjohnwu.magisk.utils.DiffObservableList -import com.topjohnwu.magisk.utils.KObservableField -import com.topjohnwu.superuser.Shell -import me.tatarka.bindingcollectionadapter2.ItemBinding -import java.io.File -import java.util.* +import com.topjohnwu.magisk.ui.base.BaseViewModel -class FlashViewModel( - action: String, - installer: Uri, - uri: Uri, - private val resources: Resources -) : BaseViewModel(), FlashResultListener { - - val canShowReboot = Shell.rootAccess() - val showRestartTitle = KObservableField(false) - - val behaviorText = KObservableField(resources.getString(R.string.flashing)) - - val items = DiffObservableList(ComparableRvItem.callback) - val itemBinding = ItemBinding.of> { itemBinding, _, item -> - item.bind(itemBinding) - itemBinding.bindExtra(BR.viewModel, this@FlashViewModel) - } - - private val outItems = ObservableArrayList() - private val logItems = Collections.synchronizedList(mutableListOf()) - - init { - outItems.sendUpdatesTo(items) { it.map { ConsoleRvItem(it) } } - outItems.copyNewInputInto(logItems) - - state = State.LOADING - - when (action) { - Const.Value.FLASH_ZIP -> Flashing - .Install(installer, outItems, logItems, this) - .exec() - Const.Value.UNINSTALL -> Flashing - .Uninstall(installer, outItems, logItems, this) - .exec() - Const.Value.FLASH_MAGISK -> Patching - .Direct(installer, outItems, logItems, this) - .exec() - Const.Value.FLASH_INACTIVE_SLOT -> Patching - .SecondSlot(installer, outItems, logItems, this) - .exec() - Const.Value.PATCH_FILE -> Patching - .File(installer, uri, outItems, logItems, this) - .exec() - } - } - - override fun onResult(isSuccess: Boolean) { - state = if (isSuccess) State.LOADED else State.LOADING_FAILED - behaviorText.value = when { - isSuccess -> resources.getString(R.string.done) - else -> resources.getString(R.string.failure) - } - - if (isSuccess) { - Handler().postDelayed(500) { - showRestartTitle.value = true - } - } - } - - fun savePressed() = withPermissions(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE) - .map { now } - .map { it.toTime(timeFormatStandard) } - .map { Const.MAGISK_INSTALL_LOG_FILENAME.format(it) } - .map { File(Config.downloadDirectory, it) } - .map { file -> - file.bufferedWriter().use { writer -> - logItems.forEach { - writer.write(it) - writer.newLine() - } - } - file.path - } - .subscribeK { SnackbarEvent(it).publish() } - .add() - - fun restartPressed() = reboot() - - fun backPressed() = back() - -} \ No newline at end of file +class FlashViewModel : BaseViewModel() diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideFragment.kt new file mode 100644 index 000000000..5ef748129 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideFragment.kt @@ -0,0 +1,86 @@ +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() { + + override val layoutRes = R.layout.fragment_hide_md2 + override val viewModel by viewModel() + + 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) + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt index 2640abcad..605041700 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt @@ -1,96 +1,123 @@ package com.topjohnwu.magisk.ui.hide import android.content.pm.ApplicationInfo +import androidx.databinding.Bindable import com.topjohnwu.magisk.BR -import com.topjohnwu.magisk.base.viewmodel.BaseViewModel +import com.topjohnwu.magisk.core.utils.currentLocale import com.topjohnwu.magisk.data.repository.MagiskRepository -import com.topjohnwu.magisk.databinding.ComparableRvItem -import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback import com.topjohnwu.magisk.extensions.subscribeK -import com.topjohnwu.magisk.extensions.toSingle -import com.topjohnwu.magisk.extensions.update -import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem -import com.topjohnwu.magisk.model.entity.recycler.HideRvItem -import com.topjohnwu.magisk.model.entity.state.IndeterminateState -import com.topjohnwu.magisk.model.events.HideProcessEvent -import com.topjohnwu.magisk.utils.DiffObservableList +import com.topjohnwu.magisk.extensions.toggle +import com.topjohnwu.magisk.model.entity.HideAppInfo +import com.topjohnwu.magisk.model.entity.HideTarget +import com.topjohnwu.magisk.model.entity.ProcessHideApp +import com.topjohnwu.magisk.model.entity.StatefulProcess +import com.topjohnwu.magisk.model.entity.recycler.HideItem +import com.topjohnwu.magisk.model.entity.recycler.HideProcessItem +import com.topjohnwu.magisk.ui.base.BaseViewModel +import com.topjohnwu.magisk.ui.base.Queryable +import com.topjohnwu.magisk.ui.base.filterableListOf +import com.topjohnwu.magisk.ui.base.itemBindingOf import com.topjohnwu.magisk.utils.KObservableField -import com.topjohnwu.magisk.utils.RxBus -import me.tatarka.bindingcollectionadapter2.OnItemBind -import timber.log.Timber class HideViewModel( - private val magiskRepo: MagiskRepository, - rxBus: RxBus -) : BaseViewModel() { + private val magiskRepo: MagiskRepository +) : BaseViewModel(), Queryable by Queryable.impl(1000) { - val query = KObservableField("") - val isShowSystem = KObservableField(false) + override val queryRunnable = Runnable { query() } - private val allItems = mutableListOf>() - val items = DiffObservableList(ComparableRvItem.callback) - val itemBinding = OnItemBind> { itemBinding, _, item -> - item.bind(itemBinding) - itemBinding.bindExtra(BR.viewModel, this@HideViewModel) + var isShowSystem = false + @Bindable get + set(value) { + field = value + notifyPropertyChanged(BR.showSystem) + query() + } + + var query = "" + @Bindable get + set(value) { + field = value + notifyPropertyChanged(BR.query) + submitQuery() + } + val items = filterableListOf() + val itemBinding = itemBindingOf { + it.bindExtra(BR.viewModel, this) + } + val itemInternalBinding = itemBindingOf { + it.bindExtra(BR.viewModel, this) } - init { - rxBus.register() - .subscribeK { toggleItem(it.item) } - .add() + val isFilterExpanded = KObservableField(false) - isShowSystem.addOnPropertyChangedCallback { query() } - query.addOnPropertyChangedCallback { query() } - - refresh() - } - - fun refresh() { - // fetching this for every item is nonsensical, so we add .cache() so the response is all - // the same for every single mapped item, it only actually executes the whole thing the - // first time around. - val hideTargets = magiskRepo.fetchHideTargets().cache() - - magiskRepo.fetchApps() - .flattenAsFlowable { it } - .map { HideRvItem(it, hideTargets.blockingGet()) } - .toList() - .map { - it.sortedWith(compareBy( - { it.isHiddenState.value }, - { it.item.name.toLowerCase() }, - { it.packageName } - )) - } - .doOnSuccess { allItems.update(it) } - .flatMap { queryRaw() } - .applyViewModel(this) - .subscribeK(onError = Timber::e) { items.update(it.first, it.second) } - .add() - } - - private fun query() = queryRaw() - .subscribeK { items.update(it.first, it.second) } - .add() - - private fun queryRaw( - showSystem: Boolean = isShowSystem.value, - query: String = this.query.value - ) = allItems.toSingle() - .map { it.filterIsInstance() } + override fun refresh() = magiskRepo.fetchApps() + .map { it to magiskRepo.fetchHideTargets().blockingGet() } + .map { pair -> pair.first.map { mergeAppTargets(it, pair.second) } } .flattenAsFlowable { it } - .filter { - it.item.name.contains(query, ignoreCase = true) || - it.item.processes.any { it.contains(query, ignoreCase = true) } - } - .filter { - showSystem || (it.isHiddenState.value != IndeterminateState.UNCHECKED) || - (it.item.info.flags and ApplicationInfo.FLAG_SYSTEM == 0) - } + .map { HideItem(it) } .toList() + .map { it.sort() } .map { it to items.calculateDiff(it) } + .applyViewModel(this) + .subscribeK { + items.update(it.first, it.second) + submitQuery() + } - private fun toggleItem(item: HideProcessRvItem) = - magiskRepo.toggleHide(item.isHidden.value, item.packageName, item.process) + // --- + + private fun mergeAppTargets(a: HideAppInfo, ts: List): ProcessHideApp { + val relevantTargets = ts.filter { it.packageName == a.info.packageName } + val packageName = a.info.packageName + val processes = a.processes + .map { StatefulProcess(it, packageName, relevantTargets.any { i -> it == i.process }) } + return ProcessHideApp(a, processes) + } + + private fun List.sort() = compareByDescending { it.itemsChecked.value } + .thenBy { it.item.info.name.toLowerCase(currentLocale) } + .thenBy { it.item.info.info.packageName } + .let { sortedWith(it) } + + // --- + + override fun submitQuery() { + queryHandler.removeCallbacks(queryRunnable) + queryHandler.postDelayed(queryRunnable, queryDelay) + } + + private fun query( + query: String = this.query, + showSystem: Boolean = isShowSystem + ) = items.filter { + fun filterSystem(): Boolean { + return showSystem || it.item.info.info.flags and ApplicationInfo.FLAG_SYSTEM == 0 + } + + fun filterQuery(): Boolean { + val inName = it.item.info.name.contains(query, true) + val inPackage = it.item.info.info.packageName.contains(query, true) + val inProcesses = it.item.processes.any { it.name.contains(query, true) } + return inName || inPackage || inProcesses + } + + filterSystem() && filterQuery() + } + + // --- + + fun toggleItem(item: HideProcessItem) = magiskRepo + .toggleHide(item.isHidden.value, item.item.packageName, item.item.name) + + fun toggle(item: KObservableField) = item.toggle() + + fun resetQuery() { + query = "" + } + + fun hideFilter() { + isFilterExpanded.value = false + } } + diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/hide/MagiskHideFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/hide/MagiskHideFragment.kt deleted file mode 100644 index f9d4c67f3..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/hide/MagiskHideFragment.kt +++ /dev/null @@ -1,71 +0,0 @@ -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(), - 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 - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeFragment.kt index d9cd0df8b..825b9943f 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeFragment.kt @@ -1,132 +1,35 @@ package com.topjohnwu.magisk.ui.home -import android.content.Context -import com.topjohnwu.magisk.BuildConfig -import com.topjohnwu.magisk.Const -import com.topjohnwu.magisk.Info +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.graphics.Insets import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.base.BaseActivity -import com.topjohnwu.magisk.base.BaseFragment -import com.topjohnwu.magisk.data.repository.MagiskRepository -import com.topjohnwu.magisk.databinding.FragmentMagiskBinding -import com.topjohnwu.magisk.extensions.DynamicClassLoader -import com.topjohnwu.magisk.extensions.openUrl -import com.topjohnwu.magisk.extensions.subscribeK -import com.topjohnwu.magisk.extensions.writeTo -import com.topjohnwu.magisk.model.events.* -import com.topjohnwu.magisk.utils.SafetyNetHelper -import com.topjohnwu.magisk.view.MarkDownWindow -import com.topjohnwu.magisk.view.dialogs.* -import com.topjohnwu.superuser.Shell -import dalvik.system.DexFile -import io.reactivex.Completable -import org.koin.android.ext.android.inject +import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding +import com.topjohnwu.magisk.model.navigation.Navigation +import com.topjohnwu.magisk.ui.base.CompatFragment import org.koin.androidx.viewmodel.ext.android.viewModel -import java.io.File -import java.lang.reflect.InvocationHandler -class HomeFragment : BaseFragment(), - SafetyNetHelper.Callback { +class HomeFragment : CompatFragment() { - override val layoutRes: Int = R.layout.fragment_magisk - override val viewModel: HomeViewModel by viewModel() + override val layoutRes = R.layout.fragment_home_md2 + override val viewModel by viewModel() - private val magiskRepo: MagiskRepository by inject() - private val EXT_APK by lazy { File("${activity.filesDir.parent}/snet", "snet.jar") } - private val EXT_DEX by lazy { File(EXT_APK.parent, "snet.dex") } - - override fun onResponse(responseCode: Int) = viewModel.finishSafetyNetCheck(responseCode) - - override fun onEventDispatched(event: ViewEvent) { - super.onEventDispatched(event) - when (event) { - is OpenLinkEvent -> activity.openUrl(event.url) - is ManagerInstallEvent -> installManager() - is MagiskInstallEvent -> installMagisk() - is UninstallEvent -> uninstall() - is ManagerChangelogEvent -> changelogManager() - is EnvFixEvent -> fixEnv() - is UpdateSafetyNetEvent -> updateSafetyNet(false) - } - } + override fun consumeSystemWindowInsets(insets: Insets) = insets override fun onStart() { super.onStart() + activity.title = resources.getString(R.string.section_home) setHasOptionsMenu(true) - requireActivity().setTitle(R.string.magisk) } - private fun installMagisk() { - // Show Manager update first - if (Info.remote.app.versionCode > BuildConfig.VERSION_CODE) { - installManager() - return - } - - MagiskInstallDialog(requireActivity() as BaseActivity<*, *>).show() + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.menu_home_md2, menu) } - private fun installManager() = ManagerInstallDialog(requireActivity()).show() - private fun uninstall() = UninstallDialog(requireActivity()).show() - private fun fixEnv() = EnvFixDialog(requireActivity()).show() + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.action_settings -> Navigation.settings().dispatchOnSelf() + else -> null + }?.let { true } ?: super.onOptionsItemSelected(item) - private fun changelogManager() = MarkDownWindow - .show(requireActivity(), null, resources.openRawResource(R.raw.changelog)) - - private fun downloadSafetyNet(requiresUserInput: Boolean = true) { - fun download() = magiskRepo.fetchSafetynet() - .map { it.byteStream().writeTo(EXT_APK) } - .subscribeK { updateSafetyNet(true) } - - if (!requiresUserInput) { - download() - return - } - - CustomAlertDialog(requireActivity()) - .setTitle(R.string.proprietary_title) - .setMessage(R.string.proprietary_notice) - .setCancelable(false) - .setPositiveButton(android.R.string.yes) { _, _ -> download() } - .setNegativeButton(android.R.string.no) { _, _ -> viewModel.finishSafetyNetCheck(-2) } - .show() - } - - private fun updateSafetyNet(dieOnError: Boolean) { - Completable.fromAction { - val loader = DynamicClassLoader(EXT_APK) - val dex = DexFile.loadDex(EXT_APK.path, EXT_DEX.path, 0) - - // Scan through the dex and find our helper class - var helperClass: Class<*>? = null - for (className in dex.entries()) { - if (className.startsWith("x.")) { - val cls = loader.loadClass(className) - if (InvocationHandler::class.java.isAssignableFrom(cls)) { - helperClass = cls - break - } - } - } - helperClass ?: throw Exception() - - val helper = helperClass.getMethod("get", - Class::class.java, Context::class.java, Any::class.java) - .invoke(null, SafetyNetHelper::class.java, activity, this) as SafetyNetHelper - - if (helper.version < Const.SNET_EXT_VER) - throw Exception() - - helper.attest() - }.subscribeK(onError = { - if (dieOnError) { - viewModel.finishSafetyNetCheck(-1) - } else { - Shell.sh("rm -rf " + EXT_APK.parent).exec() - EXT_APK.parentFile?.mkdir() - downloadSafetyNet(!dieOnError) - } - }) - } } - diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt index 2b8fc887f..64a455047 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt @@ -1,259 +1,198 @@ package com.topjohnwu.magisk.ui.home -import android.content.pm.PackageManager -import com.topjohnwu.magisk.* -import com.topjohnwu.magisk.base.viewmodel.BaseViewModel +import android.Manifest +import android.os.Build +import com.topjohnwu.magisk.BuildConfig +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.core.Config +import com.topjohnwu.magisk.core.Info +import com.topjohnwu.magisk.core.download.RemoteFileService import com.topjohnwu.magisk.data.repository.MagiskRepository import com.topjohnwu.magisk.extensions.* -import com.topjohnwu.magisk.model.events.* +import com.topjohnwu.magisk.core.model.MagiskJson +import com.topjohnwu.magisk.core.model.ManagerJson +import com.topjohnwu.magisk.core.model.UpdateInfo +import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Magisk +import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Manager +import com.topjohnwu.magisk.model.entity.recycler.DeveloperItem +import com.topjohnwu.magisk.model.entity.recycler.HomeItem +import com.topjohnwu.magisk.model.events.OpenInappLinkEvent +import com.topjohnwu.magisk.model.events.dialog.EnvFixDialog +import com.topjohnwu.magisk.model.events.dialog.ManagerInstallDialog +import com.topjohnwu.magisk.model.events.dialog.UninstallDialog +import com.topjohnwu.magisk.model.navigation.Navigation import com.topjohnwu.magisk.model.observer.Observer +import com.topjohnwu.magisk.ui.base.BaseViewModel +import com.topjohnwu.magisk.ui.base.itemBindingOf import com.topjohnwu.magisk.utils.KObservableField -import com.topjohnwu.magisk.utils.SafetyNetHelper import com.topjohnwu.superuser.Shell -import io.reactivex.Completable - -enum class SafetyNetState { - LOADING, PASS, FAILED, IDLE -} +import me.tatarka.bindingcollectionadapter2.BR +import kotlin.math.roundToInt enum class MagiskState { NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING } -enum class MagiskItem { - MANAGER, MAGISK -} - class HomeViewModel( - private val magiskRepo: MagiskRepository -) : BaseViewModel(State.LOADED) { + private val repoMagisk: MagiskRepository +) : BaseViewModel() { - val hasGMS = runCatching { - get().getPackageInfo("com.google.android.gms", 0); true - }.getOrElse { false } + val isNoticeVisible = KObservableField(Config.safetyNotice) - val isAdvancedExpanded = KObservableField(false) - - val isForceEncryption = KObservableField(Info.keepEnc) - val isKeepVerity = KObservableField(Info.keepVerity) - val isRecovery = KObservableField(Info.recovery) - - val magiskState = KObservableField(MagiskState.LOADING) - val magiskStateText = Observer(magiskState) { - when (magiskState.value) { - MagiskState.NOT_INSTALLED -> R.string.magisk_version_error.res() - MagiskState.UP_TO_DATE -> R.string.magisk_up_to_date.res() - MagiskState.LOADING -> R.string.checking_for_updates.res() - MagiskState.OBSOLETE -> R.string.magisk_update_title.res() + val stateMagisk = KObservableField(MagiskState.LOADING) + val stateManager = KObservableField(MagiskState.LOADING) + val stateTextMagisk = Observer(stateMagisk) { + when (stateMagisk.value) { + MagiskState.NOT_INSTALLED -> R.string.installed_error_md2.res() + MagiskState.UP_TO_DATE -> R.string.up_to_date_md2.res() + MagiskState.LOADING -> R.string.loading_md2.res() + MagiskState.OBSOLETE -> R.string.obsolete_md2.res() } } - val magiskCurrentVersion = KObservableField("") - val magiskLatestVersion = KObservableField("") - val magiskAdditionalInfo = Observer(magiskState) { - if (Config.coreOnly) - R.string.core_only_enabled.res() - else - "" - } - - private val _managerState = KObservableField(MagiskState.LOADING) - val managerState = Observer(_managerState, isConnected) { - if (isConnected.value) _managerState.value else MagiskState.UP_TO_DATE - } - val managerStateText = Observer(managerState) { - when (managerState.value) { - MagiskState.NOT_INSTALLED -> R.string.invalid_update_channel.res() - MagiskState.UP_TO_DATE -> R.string.manager_up_to_date.res() - MagiskState.LOADING -> R.string.checking_for_updates.res() - MagiskState.OBSOLETE -> R.string.manager_update_title.res() + val stateTextManager = Observer(stateManager) { + when (stateManager.value) { + MagiskState.NOT_INSTALLED -> R.string.channel_error_md2.res() + MagiskState.UP_TO_DATE -> R.string.up_to_date_md2.res() + MagiskState.LOADING -> R.string.loading_md2.res() + MagiskState.OBSOLETE -> R.string.obsolete_md2.res() } } - val managerCurrentVersion = KObservableField("") - val managerLatestVersion = KObservableField("") - val managerAdditionalInfo = Observer(managerState) { - if (packageName != BuildConfig.APPLICATION_ID) - "($packageName)" - else - "" - } + val statePackageManager = packageName + val statePackageOriginal = statePackageManager == BuildConfig.APPLICATION_ID + val stateVersionUpdateMagisk = KObservableField("") + val stateVersionUpdateManager = KObservableField("") - val safetyNetTitle = KObservableField(R.string.safetyNet_check_text.res()) - val ctsState = KObservableField(SafetyNetState.IDLE) - val basicIntegrityState = KObservableField(SafetyNetState.IDLE) - val safetyNetState = Observer(ctsState, basicIntegrityState) { - val cts = ctsState.value - val basic = basicIntegrityState.value - val states = listOf(cts, basic) + val stateMagiskProgress = KObservableField(0) + val stateManagerProgress = KObservableField(0) - when { - states.any { it == SafetyNetState.LOADING } -> State.LOADING - states.any { it == SafetyNetState.IDLE } -> State.LOADING - else -> State.LOADED + val stateMagiskExpanded = KObservableField(false) + val stateManagerExpanded = KObservableField(false) + + val stateHideManagerName = R.string.manager.res().let { + if (!statePackageOriginal) { + it.replaceRandomWithSpecial(3) + } else { + it } } - val isActive = KObservableField(false) + val items = listOf(DeveloperItem.Mainline, DeveloperItem.App, DeveloperItem.Project) + val itemBinding = itemBindingOf { + it.bindExtra(BR.viewModel, this) + } + val itemDeveloperBinding = itemBindingOf { + it.bindExtra(BR.viewModel, this) + } private var shownDialog = false init { - isForceEncryption.addOnPropertyChangedCallback { - Info.keepEnc = it ?: return@addOnPropertyChangedCallback - } - isKeepVerity.addOnPropertyChangedCallback { - Info.keepVerity = it ?: return@addOnPropertyChangedCallback - } - isRecovery.addOnPropertyChangedCallback { - Info.recovery = it ?: return@addOnPropertyChangedCallback - } - isConnected.addOnPropertyChangedCallback { - if (it == true) refresh(false) - } - - refresh(false) - } - - fun paypalPressed() = OpenLinkEvent(Const.Url.PAYPAL_URL).publish() - fun patreonPressed() = OpenLinkEvent(Const.Url.PATREON_URL).publish() - fun twitterPressed() = OpenLinkEvent(Const.Url.TWITTER_URL).publish() - fun githubPressed() = OpenLinkEvent(Const.Url.SOURCE_CODE_URL).publish() - fun xdaPressed() = OpenLinkEvent(Const.Url.XDA_THREAD).publish() - fun uninstallPressed() = UninstallEvent().publish() - - fun advancedPressed() = isAdvancedExpanded.toggle() - - fun installPressed(item: MagiskItem) = when (item) { - MagiskItem.MANAGER -> ManagerInstallEvent().publish() - MagiskItem.MAGISK -> MagiskInstallEvent().publish() - } - - fun cardPressed(item: MagiskItem) = when (item) { - MagiskItem.MANAGER -> ManagerChangelogEvent().publish() - MagiskItem.MAGISK -> MagiskChangelogEvent().publish() - } - - fun safetyNetPressed() { - ctsState.value = SafetyNetState.LOADING - basicIntegrityState.value = SafetyNetState.LOADING - safetyNetTitle.value = R.string.checking_safetyNet_status.res() - - UpdateSafetyNetEvent().publish() - } - - fun finishSafetyNetCheck(response: Int) = when { - response and 0x0F == 0 -> { - val hasCtsPassed = response and SafetyNetHelper.CTS_PASS != 0 - val hasBasicIntegrityPassed = response and SafetyNetHelper.BASIC_PASS != 0 - safetyNetTitle.value = R.string.safetyNet_check_success.res() - ctsState.value = if (hasCtsPassed) { - SafetyNetState.PASS - } else { - SafetyNetState.FAILED - } - basicIntegrityState.value = if (hasBasicIntegrityPassed) { - SafetyNetState.PASS - } else { - SafetyNetState.FAILED - } - } - response == -2 -> { - ctsState.value = SafetyNetState.IDLE - basicIntegrityState.value = SafetyNetState.IDLE - } - else -> { - ctsState.value = SafetyNetState.IDLE - basicIntegrityState.value = SafetyNetState.IDLE - safetyNetTitle.value = when (response) { - SafetyNetHelper.RESPONSE_ERR -> R.string.safetyNet_res_invalid.res() - else -> R.string.safetyNet_api_error.res() + RemoteFileService.progressBroadcast.observeForever { + when (it?.second) { + is Magisk.Download, + is Magisk.Flash -> stateMagiskProgress.value = it.first.times(100f).roundToInt() + is Manager -> stateManagerProgress.value = it.first.times(100f).roundToInt() } } } - @JvmOverloads - fun refresh(invalidate: Boolean = true) { - if (invalidate) - Info.envRef.invalidate() + override fun refresh() = repoMagisk.fetchUpdate() + .onErrorReturn { Info.remote } + .subscribeK { updateBy(it) } - isActive.value = Info.env.isActive - - 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() - refreshVersions() - } - } - - private fun refreshVersions() { - 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 - .format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, Info.stub!!.version) - else - VERSION_FMT.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE) - } - - private fun updateSelf() { - magiskState.value = when (Info.env.magiskVersionCode) { - in Int.MIN_VALUE .. 0 -> MagiskState.NOT_INSTALLED - in 1 until Info.remote.magisk.versionCode -> MagiskState.OBSOLETE + private fun updateBy(info: UpdateInfo) { + stateMagisk.value = when { + !info.magisk.isInstalled -> MagiskState.NOT_INSTALLED + info.magisk.isObsolete -> MagiskState.OBSOLETE else -> MagiskState.UP_TO_DATE } - magiskLatestVersion.value = - VERSION_FMT.format(Info.remote.magisk.version, Info.remote.magisk.versionCode) - - _managerState.value = when (Info.remote.app.versionCode) { - in Int.MIN_VALUE .. 0 -> MagiskState.NOT_INSTALLED //wrong update channel - in (BuildConfig.VERSION_CODE + 1) .. Int.MAX_VALUE -> MagiskState.OBSOLETE - else -> { - if (Info.stub?.version ?: Int.MAX_VALUE < Info.remote.stub.versionCode) - MagiskState.OBSOLETE - else - MagiskState.UP_TO_DATE - } + stateManager.value = when { + !info.app.isUpdateChannelCorrect && isConnected.value -> MagiskState.NOT_INSTALLED + info.app.isObsolete -> MagiskState.OBSOLETE + else -> MagiskState.UP_TO_DATE } - managerLatestVersion.value = MGR_VER_FMT - .format(Info.remote.app.version, Info.remote.app.versionCode, Info.remote.stub.versionCode) + stateVersionUpdateMagisk.value = when { + info.magisk.isObsolete -> "%s > %s".format( + Info.env.magiskVersionString.clipVersion(info.magisk.version), + info.magisk.version.clipVersion(Info.env.magiskVersionString) + ) + else -> "" + } + + stateVersionUpdateManager.value = when { + info.app.isObsolete -> "%s > %s".format( + BuildConfig.VERSION_NAME.clipVersion(info.app.version), + info.app.version.clipVersion(BuildConfig.VERSION_NAME) + ) + else -> "" + } + + ensureEnv() + } + + fun onLinkPressed(link: String) = OpenInappLinkEvent(link).publish() + + fun onDeletePressed() = UninstallDialog().publish() + + fun onManagerPressed() = ManagerInstallDialog().publish() + + fun onMagiskPressed() = withPermissions( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ).map { check(it);it } + .subscribeK { Navigation.install().publish() } + .add() + + fun toggle(kof: KObservableField) = kof.toggle() + + fun hideNotice() { + Config.safetyNotice = false + isNoticeVisible.value = false } private fun ensureEnv() { - val invalidStates = - listOf(MagiskState.NOT_INSTALLED, MagiskState.LOADING) + val invalidStates = listOf( + MagiskState.NOT_INSTALLED, + MagiskState.LOADING + ) // Don't bother checking env when magisk is not installed, loading or already has been shown - if (invalidStates.any { it == magiskState.value } || shownDialog) return - - if (!Shell.su("env_check").exec().isSuccess) { - shownDialog = true - EnvFixEvent().publish() + if ( + invalidStates.any { it == stateMagisk.value } || + shownDialog || + // don't care for emulators either + Build.DEVICE.orEmpty().contains("generic") || + Build.PRODUCT.orEmpty().contains("generic") + ) { + return } + + Shell.su("env_check") + .toSingle() + .map { it.exec() } + .filter { !it.isSuccess } + .subscribeK { + shownDialog = true + EnvFixDialog().publish() + } } - companion object { - private const val VERSION_FMT = "%s (%d)" - private const val MGR_VER_FMT = "%s (%d) (%d)" + private fun String.clipVersion(other: String = ""): String { + val thisVersion = substringBefore('-') + val otherVersion = other.substringBefore('-') + return if (thisVersion != otherVersion) thisVersion else substringAfter('-') } } + +@Suppress("unused") +val MagiskJson.isInstalled + get() = Info.env.magiskVersionCode > 0 +val MagiskJson.isObsolete + get() = Info.env.magiskVersionCode < versionCode && isInstalled +val ManagerJson.isUpdateChannelCorrect + get() = versionCode > 0 +val ManagerJson.isObsolete + get() = BuildConfig.VERSION_CODE < versionCode diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt new file mode 100644 index 000000000..6cdfbb1e5 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt @@ -0,0 +1,28 @@ +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() { + + override val layoutRes = R.layout.fragment_install_md2 + override val viewModel by viewModel() + + 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) + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt new file mode 100644 index 000000000..aec2bafdd --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt @@ -0,0 +1,72 @@ +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(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() } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.kt index 24f0e1db2..29db680cd 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.kt @@ -1,57 +1,76 @@ package com.topjohnwu.magisk.ui.log - import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View +import androidx.core.graphics.Insets +import androidx.core.view.isVisible import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.base.BaseFragment -import com.topjohnwu.magisk.databinding.FragmentLogBinding -import com.topjohnwu.magisk.model.events.PageChangedEvent -import com.topjohnwu.magisk.model.events.ViewEvent +import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding +import com.topjohnwu.magisk.ui.MainActivity +import com.topjohnwu.magisk.ui.base.CompatFragment +import com.topjohnwu.magisk.utils.MotionRevealHelper import org.koin.androidx.viewmodel.ext.android.viewModel -class LogFragment : BaseFragment() { +class LogFragment : CompatFragment() { - override val layoutRes: Int = R.layout.fragment_log - override val viewModel: LogViewModel by viewModel() + override val layoutRes = R.layout.fragment_log_md2 + override val viewModel by viewModel() - override fun onEventDispatched(event: ViewEvent) { - super.onEventDispatched(event) - when (event) { - is PageChangedEvent -> activity.invalidateOptionsMenu() + private var actionSave: MenuItem? = null + private var isMagiskLogVisible + get() = binding.logFilter.isVisible + set(value) { + MotionRevealHelper.withViews(binding.logFilter, binding.logFilterToggle, value) + actionSave?.isVisible = value + (activity as MainActivity).invalidateToolbar() } - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.logTabs.setupWithViewPager(binding.logContainer, true) - } + override fun consumeSystemWindowInsets(insets: Insets) = insets override fun onStart() { super.onStart() setHasOptionsMenu(true) - activity.setTitle(R.string.log) + activity.title = resources.getString(R.string.section_log) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.logFilterToggle.setOnClickListener { + isMagiskLogVisible = true + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.menu_log, menu) - menu.findItem(R.id.menu_save).isVisible = viewModel.currentPage.value == 1 + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.menu_log_md2, menu) + actionSave = menu.findItem(R.id.action_save)?.also { + it.isVisible = isMagiskLogVisible + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.menu_save -> activity.withExternalRW { - onSuccess { - viewModel.saveLog() - } - } - R.id.menu_clear -> viewModel.clearLog() - R.id.menu_refresh -> viewModel.refresh() + R.id.action_save -> viewModel.saveMagiskLog() + R.id.action_clear -> + if (isMagiskLogVisible) viewModel.clearMagiskLog() + else viewModel.clearLog() } - return true + return super.onOptionsItemSelected(item) + } + + + override fun onPreBind(binding: FragmentLogMd2Binding) = Unit + + override fun onBackPressed(): Boolean { + if (binding.logFilter.isVisible) { + isMagiskLogVisible = false + return true + } + return super.onBackPressed() } } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt index d3d50e3fd..0e39b71c3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt @@ -1,77 +1,81 @@ package com.topjohnwu.magisk.ui.log -import android.content.res.Resources import com.topjohnwu.magisk.BR -import com.topjohnwu.magisk.Config -import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.base.viewmodel.BaseViewModel +import com.topjohnwu.magisk.core.Config +import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.data.repository.LogRepository import com.topjohnwu.magisk.databinding.ComparableRvItem -import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback -import com.topjohnwu.magisk.extensions.doOnSubscribeUi import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.model.binding.BindingAdapter -import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem -import com.topjohnwu.magisk.model.entity.recycler.LogItemRvItem -import com.topjohnwu.magisk.model.entity.recycler.LogRvItem -import com.topjohnwu.magisk.model.entity.recycler.MagiskLogRvItem -import com.topjohnwu.magisk.model.events.PageChangedEvent +import com.topjohnwu.magisk.model.entity.recycler.ConsoleItem +import com.topjohnwu.magisk.model.entity.recycler.LogItem +import com.topjohnwu.magisk.model.entity.recycler.TextItem import com.topjohnwu.magisk.model.events.SnackbarEvent -import com.topjohnwu.magisk.utils.DiffObservableList -import com.topjohnwu.magisk.utils.KObservableField +import com.topjohnwu.magisk.ui.base.BaseViewModel +import com.topjohnwu.magisk.ui.base.diffListOf +import com.topjohnwu.magisk.ui.base.itemBindingOf import com.topjohnwu.superuser.Shell -import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter -import me.tatarka.bindingcollectionadapter2.OnItemBind +import io.reactivex.Completable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers import timber.log.Timber import java.io.File import java.util.* class LogViewModel( - private val resources: Resources, - private val logRepo: LogRepository -) : BaseViewModel(), BindingViewPagerAdapter.PageTitles> { + private val repo: LogRepository +) : BaseViewModel() { - val itemsAdapter = BindingAdapter() - val items = DiffObservableList(ComparableRvItem.callback) - val itemBinding = OnItemBind> { itemBinding, _, item -> - item.bind(itemBinding) - itemBinding.bindExtra(BR.viewModel, this@LogViewModel) - } - val currentPage = KObservableField(0) - private val currentItem get() = items[currentPage.value] + // --- empty view - private val logItem get() = items[0] as LogRvItem - private val magiskLogItem get() = items[1] as MagiskLogRvItem + val itemEmpty = TextItem(R.string.log_data_none) + val itemMagiskEmpty = TextItem(R.string.log_data_magisk_none) - val scrollPosition = KObservableField(0) + // --- main view - init { - currentPage.addOnPropertyChangedCallback { - it ?: return@addOnPropertyChangedCallback - PageChangedEvent().publish() - } - - items.addAll(listOf(LogRvItem(), MagiskLogRvItem())) - refresh() + val items = diffListOf() + val itemBinding = itemBindingOf { + it.bindExtra(BR.viewModel, this) } - override fun getPageTitle(position: Int, item: ComparableRvItem<*>?) = when (item) { - is LogRvItem -> resources.getString(R.string.superuser) - is MagiskLogRvItem -> resources.getString(R.string.magisk) - else -> "" + // --- console + + val consoleAdapter = BindingAdapter() + val itemsConsole = diffListOf>() + val itemConsoleBinding = itemBindingOf> {} + + override fun refresh(): Disposable { + val logs = repo.fetchLogs() + .map { it.map { LogItem(it) } } + .observeOn(Schedulers.computation()) + .map { it to items.calculateDiff(it) } + .observeOn(AndroidSchedulers.mainThread()) + .doOnSuccess { + items.firstOrNull()?.isTop = false + items.lastOrNull()?.isBottom = false + + items.update(it.first, it.second) + + items.firstOrNull()?.isTop = true + items.lastOrNull()?.isBottom = true + } + .ignoreElement() + + val console = repo.fetchMagiskLogs() + .map { ConsoleItem(it) } + .toList() + .observeOn(Schedulers.computation()) + .map { it to itemsConsole.calculateDiff(it) } + .observeOn(AndroidSchedulers.mainThread()) + .doOnSuccess { itemsConsole.update(it.first, it.second) } + .ignoreElement() + + return Completable.merge(listOf(logs, console)).subscribeK() } - fun scrollDownPressed() { - scrollPosition.value = magiskLogItem.items.size - 1 - } - - fun refresh() { - fetchLogs().subscribeK { logItem.update(it) } - fetchMagiskLog().subscribeK { magiskLogItem.update(it) } - } - - fun saveLog() { + fun saveMagiskLog() { val now = Calendar.getInstance() val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format( now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1, @@ -92,29 +96,18 @@ class LogViewModel( } } - fun clearLog() = when (currentItem) { - is LogRvItem -> clearLogs { refresh() } - is MagiskLogRvItem -> clearMagiskLogs { refresh() } - else -> Unit - } - - private fun clearLogs(callback: () -> Unit) = logRepo.clearLogs() - .doOnSubscribeUi(callback) - .subscribeK { SnackbarEvent(R.string.logs_cleared).publish() } + fun clearMagiskLog() = repo.clearMagiskLogs() + .subscribeK { + SnackbarEvent(R.string.logs_cleared).publish() + requestRefresh() + } .add() - private fun clearMagiskLogs(callback: () -> Unit) = logRepo.clearMagiskLogs() - .doOnComplete(callback) - .subscribeK { SnackbarEvent(R.string.logs_cleared).publish() } + fun clearLog() = repo.clearLogs() + .subscribeK { + SnackbarEvent(R.string.logs_cleared).publish() + requestRefresh() + } .add() - private fun fetchLogs() = logRepo.fetchLogs() - .flattenAsFlowable { it } - .map { LogItemRvItem(it) } - .toList() - - private fun fetchMagiskLog() = logRepo.fetchMagiskLogs() - .map { ConsoleRvItem(it) } - .toList() - } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt new file mode 100644 index 000000000..3167d27cd --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt @@ -0,0 +1,151 @@ +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(), + ReselectionTarget { + + override val layoutRes = R.layout.fragment_module_md2 + override val viewModel by viewModel() + + private val listeners = hashSetOf() + + 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) + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt index 1ac4ed62b..97c04cf19 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt @@ -1,111 +1,322 @@ package com.topjohnwu.magisk.ui.module -import android.content.res.Resources +import androidx.annotation.WorkerThread +import androidx.databinding.Bindable +import androidx.databinding.ObservableArrayList import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.base.viewmodel.BaseViewModel -import com.topjohnwu.magisk.data.database.RepoDao +import com.topjohnwu.magisk.core.Config +import com.topjohnwu.magisk.core.download.RemoteFileService +import com.topjohnwu.magisk.core.model.module.Module +import com.topjohnwu.magisk.core.model.module.Repo +import com.topjohnwu.magisk.core.tasks.RepoUpdater +import com.topjohnwu.magisk.data.database.RepoByNameDao +import com.topjohnwu.magisk.data.database.RepoByUpdatedDao import com.topjohnwu.magisk.databinding.ComparableRvItem -import com.topjohnwu.magisk.extensions.* -import com.topjohnwu.magisk.model.entity.module.Module -import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem -import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem -import com.topjohnwu.magisk.model.entity.recycler.SectionRvItem -import com.topjohnwu.magisk.model.events.InstallModuleEvent +import com.topjohnwu.magisk.extensions.reboot +import com.topjohnwu.magisk.extensions.subscribeK +import com.topjohnwu.magisk.model.entity.internal.DownloadSubject +import com.topjohnwu.magisk.model.entity.recycler.* +import com.topjohnwu.magisk.model.events.InstallExternalModuleEvent import com.topjohnwu.magisk.model.events.OpenChangelogEvent -import com.topjohnwu.magisk.model.events.OpenFilePickerEvent -import com.topjohnwu.magisk.tasks.RepoUpdater -import com.topjohnwu.magisk.utils.DiffObservableList +import com.topjohnwu.magisk.model.events.dialog.ModuleInstallDialog +import com.topjohnwu.magisk.ui.base.* +import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener import com.topjohnwu.magisk.utils.KObservableField import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable -import me.tatarka.bindingcollectionadapter2.OnItemBind +import io.reactivex.schedulers.Schedulers +import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList +import timber.log.Timber +import kotlin.math.roundToInt + +/* +* The repo fetching behavior should follow these rules: +* +* For the first time the repo list is queried in the app, it should ALWAYS fetch for +* updates. However, this particular fetch should go through RepoUpdater.invoke(false), +* which internally will set ETAGs when doing GET requests to GitHub's API and will +* only update repo DB only if the GitHub API shows that something is changed remotely. +* +* When a user explicitly requests a full DB refresh, it should ALWAYS do a full force +* refresh, which in code can be done with RepoUpdater.invoke(true). This will update +* every single repo's information regardless whether GitHub's API shows if there is +* anything changed or not. +* */ class ModuleViewModel( - private val resources: Resources, - private val repoUpdater: RepoUpdater, - private val repoDB: RepoDao -) : BaseViewModel() { + private val repoName: RepoByNameDao, + private val repoUpdated: RepoByUpdatedDao, + private val repoUpdater: RepoUpdater +) : BaseViewModel(), Queryable by Queryable.impl(1000) { - val query = KObservableField("") + override val queryRunnable = Runnable { query() } - private val allItems = mutableListOf>() - - val itemsInstalled = DiffObservableList(ComparableRvItem.callback) - val itemsRemote = DiffObservableList(ComparableRvItem.callback) - val itemBinding = OnItemBind> { itemBinding, _, item -> - item.bind(itemBinding) - itemBinding.bindExtra(BR.viewModel, this@ModuleViewModel) - } - - private var queryDisposable: Disposable? = null - - init { - query.addOnPropertyChangedCallback { - queryDisposable?.dispose() - queryDisposable = query() - } - refresh(false) - } - - fun fabPressed() = OpenFilePickerEvent().publish() - fun repoPressed(item: RepoRvItem) = OpenChangelogEvent(item.item).publish() - fun downloadPressed(item: RepoRvItem) = InstallModuleEvent(item.item).publish() - - fun refresh(force: Boolean) { - Single.fromCallable { Module.loadModules() } - .flattenAsFlowable { it } - .map { ModuleRvItem(it) } - .toList() - .map { it to itemsInstalled.calculateDiff(it) } - .doOnSuccessUi { itemsInstalled.update(it.first, it.second) } - .flatMap { repoUpdater(force) } - .flattenAsFlowable { repoDB.repos } - .map { RepoRvItem(it) } - .toList() - .doOnSuccess { allItems.update(it) } - .flatMap { queryRaw() } - .applyViewModel(this) - .subscribeK { itemsRemote.update(it.first, it.second) } - } - - private fun query() = queryRaw() - .subscribeK { itemsRemote.update(it.first, it.second) } - - private fun queryRaw(query: String = this.query.value) = allItems.toSingle() - .map { it.filterIsInstance() } - .flattenAsFlowable { it } - .filter { - it.item.name.contains(query, ignoreCase = true) || - it.item.author.contains(query, ignoreCase = true) || - 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.divide(): List> { - val installed = itemsInstalled.filterIsInstance() - - fun > List.withTitle(text: Int) = - if (isEmpty()) this else listOf(SectionRvItem(resources.getString(text))) + this - - val groupedItems = groupBy { repo -> - installed.firstOrNull { it.item.id == repo.item.id }?.let { - if (it.item.versionCode < repo.item.versionCode) MODULE_UPDATABLE - else MODULE_INSTALLED - } ?: MODULE_REMOTE + 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 } - return groupedItems.getOrElse(MODULE_UPDATABLE) { listOf() }.withTitle(R.string.update_available) + - groupedItems.getOrElse(MODULE_INSTALLED) { listOf() }.withTitle(R.string.installed) + - groupedItems.getOrElse(MODULE_REMOTE) { listOf() }.withTitle(R.string.not_installed) + private var queryJob: Disposable? = null + val searchLoading = KObservableField(false) + val itemsSearch = diffListOf() + val itemSearchBinding = itemBindingOf { + it.bindExtra(BR.viewModel, this) + } + + private val itemNoneInstalled = TextItem(R.string.module_install_none) + private val itemNoneUpdatable = TextItem(R.string.module_update_none) + + private val itemsInstalledHelpers = ObservableArrayList().also { + it.add(itemNoneInstalled) + } + private val itemsUpdatableHelpers = ObservableArrayList().also { + it.add(itemNoneUpdatable) + } + + private val itemsCoreOnly = ObservableArrayList() + private val itemsInstalled = diffListOf() + private val itemsUpdatable = diffListOf() + private val itemsRemote = diffListOf() + + val adapter = adapterOf>() + val items = MergeObservableList>() + .insertList(itemsCoreOnly) + .insertItem(sectionActive) + .insertList(itemsInstalledHelpers) + .insertList(itemsInstalled) + .insertItem(InstallModule) + .insertItem(sectionUpdate) + .insertList(itemsUpdatableHelpers) + .insertList(itemsUpdatable) + .insertItem(sectionRemote) + .insertList(itemsRemote)!! + val itemBinding = itemBindingOf> { + it.bindExtra(BR.viewModel, this) } companion object { - protected const val MODULE_INSTALLED = 0 - protected const val MODULE_REMOTE = 1 - protected const val MODULE_UPDATABLE = 2 + 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 { + updateOrderIcon() + } + + 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 + } + } } -} \ No newline at end of file + // --- + + private var remoteJob: Disposable? = null + private var refetch = false + private val dao + get() = when (Config.repoOrder) { + Config.Value.ORDER_DATE -> repoUpdated + Config.Value.ORDER_NAME -> repoName + else -> throw IllegalArgumentException() + } + + // --- + + init { + RemoteFileService.reset() + RemoteFileService.progressBroadcast.observeForever { + val (progress, subject) = it ?: return@observeForever + if (subject !is DownloadSubject.Module) { + return@observeForever + } + update(subject.module, progress.times(100).roundToInt()) + } + } + + // --- + + override fun refresh(): Disposable { + updateCoreOnlyWarning() + if (itemsRemote.isEmpty()) + loadRemote() + return loadInstalled().subscribeK() + } + + private fun loadInstalled() = Single.fromCallable { Module.loadModules() } + .map { it.map { ModuleItem(it) } } + .map { it.loadDetail() } + .map { it to itemsInstalled.calculateDiff(it) } + .applyViewModel(this) + .observeOn(AndroidSchedulers.mainThread()) + .map { + itemsInstalled.update(it.first, it.second) + if (itemsInstalled.isNotEmpty()) + itemsInstalledHelpers.remove(itemNoneInstalled) + it.first + } + .observeOn(Schedulers.io()) + .map { loadUpdates(it) } + .map { it to itemsUpdatable.calculateDiff(it) } + .observeOn(AndroidSchedulers.mainThread()) + .doOnSuccess { + itemsUpdatable.update(it.first, it.second) + if (itemsUpdatable.isNotEmpty()) + itemsUpdatableHelpers.remove(itemNoneUpdatable) + } + .ignoreElement()!! + + @Synchronized + 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 + } + + fun forceRefresh() { + itemsRemote.clear() + itemsSearch.clear() + refetch = true + refresh() + submitQuery() + } + + // --- + + override fun submitQuery() { + queryHandler.removeCallbacks(queryRunnable) + queryHandler.postDelayed(queryRunnable, queryDelay) + } + + private fun queryInternal(query: String, offset: Int): Single> { + if (query.isBlank()) { + return Single.just(listOf()) + .doOnSubscribe { itemsSearch.clear() } + .subscribeOn(AndroidSchedulers.mainThread()) + } + return Single.fromCallable { dao.searchRepos(query, offset) } + .map { it.map { RepoItem.Remote(it) } } + } + + private fun query(query: String = this.query, offset: Int = 0) { + queryJob?.dispose() + queryJob = queryInternal(query, offset) + .map { it to itemsSearch.calculateDiff(it) } + .observeOn(AndroidSchedulers.mainThread()) + .doOnSuccess { searchLoading.value = false } + .subscribeK { itemsSearch.update(it.first, it.second) } + } + + @Synchronized + fun loadMoreQuery() { + if (queryJob?.isDisposed == false) return + queryJob = queryInternal(query, itemsSearch.size) + .subscribeK { itemsSearch.addAll(it) } + } + + // --- + + @WorkerThread + private fun List.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) = 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() + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.kt deleted file mode 100644 index ba2520707..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.kt +++ /dev/null @@ -1,99 +0,0 @@ -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() { - - 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() - 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) - } - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt deleted file mode 100644 index 70ebb78c5..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt +++ /dev/null @@ -1,116 +0,0 @@ -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(), - 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() - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/request/RequestActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/request/RequestActivity.kt new file mode 100644 index 000000000..c660fad7d --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/request/RequestActivity.kt @@ -0,0 +1,14 @@ +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() { + + override val navHost = TODO() + override val layoutRes = R.layout.activity_request_md2 + override val viewModel by viewModel() + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/request/RequestViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/request/RequestViewModel.kt new file mode 100644 index 000000000..67f28c8e9 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/request/RequestViewModel.kt @@ -0,0 +1,5 @@ +package com.topjohnwu.magisk.ui.request + +import com.topjohnwu.magisk.ui.base.BaseViewModel + +class RequestViewModel : BaseViewModel() diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetFragment.kt new file mode 100644 index 000000000..f1de04f02 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetFragment.kt @@ -0,0 +1,18 @@ +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() { + + override val layoutRes = R.layout.fragment_safetynet_md2 + override val viewModel by viewModel() + + override fun onStart() { + super.onStart() + activity.setTitle(R.string.safetyNet) + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt new file mode 100644 index 000000000..3bf21b61a --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt @@ -0,0 +1,96 @@ +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() + .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 + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt index 04a3485a1..3d25b1a37 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt @@ -1,345 +1,40 @@ package com.topjohnwu.magisk.ui.settings -import android.content.SharedPreferences -import android.os.Build import android.os.Bundle -import android.os.Environment -import android.view.LayoutInflater -import android.widget.EditText -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.databinding.DataBindingUtil -import androidx.preference.ListPreference -import androidx.preference.Preference -import androidx.preference.PreferenceCategory -import androidx.preference.SwitchPreferenceCompat -import com.topjohnwu.magisk.* -import com.topjohnwu.magisk.base.BasePreferenceFragment -import com.topjohnwu.magisk.data.database.RepoDao -import com.topjohnwu.magisk.databinding.CustomDownloadDialogBinding -import com.topjohnwu.magisk.databinding.DialogCustomNameBinding -import com.topjohnwu.magisk.extensions.subscribeK -import com.topjohnwu.magisk.model.download.DownloadService -import com.topjohnwu.magisk.model.entity.internal.Configuration -import com.topjohnwu.magisk.model.entity.internal.DownloadSubject -import com.topjohnwu.magisk.model.observer.Observer -import com.topjohnwu.magisk.utils.* -import com.topjohnwu.superuser.Shell -import io.reactivex.Completable -import org.koin.android.ext.android.inject -import java.io.File +import android.view.View +import androidx.core.graphics.Insets +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.databinding.FragmentSettingsMd2Binding +import com.topjohnwu.magisk.ui.base.CompatFragment +import com.topjohnwu.magisk.utils.PinchZoomTouchListener +import org.koin.androidx.viewmodel.ext.android.viewModel -class SettingsFragment : BasePreferenceFragment() { +class SettingsFragment : CompatFragment() { - private val repoDB: RepoDao by inject() + override val layoutRes = R.layout.fragment_settings_md2 + override val viewModel by viewModel() - private lateinit var updateChannel: ListPreference - private lateinit var autoRes: ListPreference - private lateinit var suNotification: ListPreference - private lateinit var requestTimeout: ListPreference - private lateinit var rootConfig: ListPreference - private lateinit var multiuserConfig: ListPreference - private lateinit var nsConfig: ListPreference + override fun consumeSystemWindowInsets(insets: Insets) = insets override fun onStart() { super.onStart() - setHasOptionsMenu(true) - requireActivity().setTitle(R.string.settings) + + activity.title = resources.getString(R.string.section_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(Config.Key.SU_REAUTH)!! - val biometric = findPreference(Config.Key.SU_BIOMETRIC)!! - val generalCategory = findPreference("general")!! - val magiskCategory = findPreference("magisk")!! - val suCategory = findPreference("superuser")!! - val hideManager = findPreference("hide")!! - val restoreManager = findPreference("restore")!! - - // Remove/Disable entries - - // Only show canary channels if user is already on canary channel - // or the user have already chosen canary channel - if (!Utils.isCanary && Config.updateChannel < Config.Value.CANARY_CHANNEL) { - // Remove the last 2 entries - val entries = updateChannel.entries - updateChannel.entries = entries.copyOf(entries.size - 2) - } - - // Remove dangerous settings in secondary user - if (Const.USER_ID > 0) { - suCategory.removePreference(multiuserConfig) - } - - // Remove re-authentication option on Android O, it will not work - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - suCategory.removePreference(reauth) - } - - // Disable biometric option if not possible - if (!BiometricHelper.isSupported) { - biometric.isEnabled = false - biometric.isChecked = false - biometric.setSummary(R.string.no_biometric) - } - - if (Const.USER_ID == 0 && Info.isConnected.value && Info.env.isActive) { - if (activity.packageName == BuildConfig.APPLICATION_ID) { - generalCategory.removePreference(restoreManager) - hideManager.setOnPreferenceClickListener { - showManagerNameDialog { - PatchAPK.hideManager(requireContext(), it) - } - true - } - } else { - generalCategory.removePreference(hideManager) - restoreManager.setOnPreferenceClickListener { - DownloadService(requireContext()) { - subject = DownloadSubject.Manager(Configuration.APK.Restore) - } - true - } - } - } else { - // Remove if not primary user, no connection, or no root - generalCategory.removePreference(restoreManager) - generalCategory.removePreference(hideManager) - } - - if (!Utils.showSuperUser()) { - preferenceScreen.removePreference(suCategory) - } - - if (!Info.env.isActive) { - preferenceScreen.removePreference(magiskCategory) - generalCategory.removePreference(hideManager) - } - - findPreference("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("hosts")?.setOnPreferenceClickListener { - Shell.su("add_hosts_module").submit { - Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT) - } - true - } - - findPreference(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 onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + PinchZoomTouchListener.attachTo(binding.settingsList) } - 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 onDestroyView() { + PinchZoomTouchListener.clear(binding.settingsList) + super.onDestroyView() } - override fun onPreferenceTreeClick(preference: Preference): Boolean { - when (preference.key) { - Config.Key.SU_BIOMETRIC -> { - val checked = (preference as SwitchPreferenceCompat).isChecked - preference.isChecked = !checked - BiometricHelper.authenticate(requireActivity()) { - preference.isChecked = checked - Config.suBiometric = checked - } - } - } - return true + override fun onResume() { + super.onResume() + viewModel.items.forEach { it.refresh() } } - 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(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)) - } } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt new file mode 100644 index 000000000..87b6b73b1 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt @@ -0,0 +1,320 @@ +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() + override var entryValues = arrayOf() + + 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().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() + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt new file mode 100644 index 000000000..68bf24abb --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt @@ -0,0 +1,159 @@ +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() + val itemBinding = itemBindingOf { it.bindExtra(BR.callback, this) } + val items = diffListOf(createItems()) + + private fun createItems(): List { + // 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() + 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) + } + } + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.kt index 00386b01e..4e674649c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.kt @@ -1,25 +1,48 @@ package com.topjohnwu.magisk.ui.superuser +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.core.graphics.Insets import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.base.BaseFragment -import com.topjohnwu.magisk.databinding.FragmentSuperuserBinding +import com.topjohnwu.magisk.databinding.FragmentSuperuserMd2Binding +import com.topjohnwu.magisk.model.navigation.Navigation +import com.topjohnwu.magisk.ui.base.CompatFragment +import com.topjohnwu.magisk.utils.PinchZoomTouchListener import org.koin.androidx.viewmodel.ext.android.viewModel -class SuperuserFragment : - BaseFragment() { +class SuperuserFragment : CompatFragment() { - override val layoutRes: Int = R.layout.fragment_superuser - override val viewModel: SuperuserViewModel by viewModel() + override val layoutRes = R.layout.fragment_superuser_md2 + override val viewModel by viewModel() + + override fun consumeSystemWindowInsets(insets: Insets) = insets override fun onStart() { super.onStart() + activity.title = resources.getString(R.string.section_superuser) setHasOptionsMenu(true) - requireActivity().setTitle(R.string.superuser) } - override fun onResume() { - super.onResume() - viewModel.updatePolicies() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + PinchZoomTouchListener.attachTo(binding.superuserList) } + override fun onDestroyView() { + PinchZoomTouchListener.clear(binding.superuserList) + super.onDestroyView() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.menu_superuser_md2, menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.action_log -> Navigation.log().dispatchOnSelf() + else -> null + }?.let { true } ?: super.onOptionsItemSelected(item) + } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt index 57f24bcc5..a538e3254 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt @@ -2,150 +2,162 @@ package com.topjohnwu.magisk.ui.superuser import android.content.pm.PackageManager import android.content.res.Resources +import androidx.databinding.ObservableArrayList import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.base.viewmodel.BaseViewModel -import com.topjohnwu.magisk.data.database.PolicyDao +import com.topjohnwu.magisk.core.magiskdb.PolicyDao +import com.topjohnwu.magisk.core.model.MagiskPolicy +import com.topjohnwu.magisk.core.utils.currentLocale import com.topjohnwu.magisk.databinding.ComparableRvItem import com.topjohnwu.magisk.extensions.applySchedulers import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.extensions.toggle -import com.topjohnwu.magisk.model.entity.MagiskPolicy -import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem -import com.topjohnwu.magisk.model.events.PolicyEnableEvent +import com.topjohnwu.magisk.model.entity.recycler.PolicyItem +import com.topjohnwu.magisk.model.entity.recycler.TappableHeadlineItem +import com.topjohnwu.magisk.model.entity.recycler.TextItem import com.topjohnwu.magisk.model.events.PolicyUpdateEvent import com.topjohnwu.magisk.model.events.SnackbarEvent -import com.topjohnwu.magisk.utils.BiometricHelper -import com.topjohnwu.magisk.utils.DiffObservableList -import com.topjohnwu.magisk.utils.RxBus -import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog +import com.topjohnwu.magisk.model.events.dialog.BiometricDialog +import com.topjohnwu.magisk.model.events.dialog.SuperuserRevokeDialog +import com.topjohnwu.magisk.model.navigation.Navigation +import com.topjohnwu.magisk.ui.base.BaseViewModel +import com.topjohnwu.magisk.ui.base.adapterOf +import com.topjohnwu.magisk.ui.base.diffListOf +import com.topjohnwu.magisk.ui.base.itemBindingOf +import com.topjohnwu.magisk.core.utils.BiometricHelper import io.reactivex.Single -import io.reactivex.disposables.Disposable -import me.tatarka.bindingcollectionadapter2.ItemBinding +import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList class SuperuserViewModel( - private val policyDB: PolicyDao, + private val db: PolicyDao, private val packageManager: PackageManager, - private val resources: Resources, - rxBus: RxBus -) : BaseViewModel() { + private val resources: Resources +) : BaseViewModel(), TappableHeadlineItem.Listener { - val items = DiffObservableList(ComparableRvItem.callback) - val itemBinding = ItemBinding.of> { itemBinding, _, item -> - item.bind(itemBinding) - itemBinding.bindExtra(BR.viewModel, this@SuperuserViewModel) + private val itemNoData = TextItem(R.string.superuser_policy_none) + + private val itemsPolicies = diffListOf() + private val itemsHelpers = ObservableArrayList().also { + it.add(itemNoData) } - private var ignoreNext: PolicyRvItem? = null - private var fetchTask: Disposable? = null + val adapter = adapterOf>() + val items = MergeObservableList>() + .insertItem(TappableHeadlineItem.Hide) + .insertItem(TappableHeadlineItem.Safetynet) + .insertList(itemsHelpers) + .insertList(itemsPolicies) + val itemBinding = itemBindingOf> { + it.bindExtra(BR.viewModel, this) + it.bindExtra(BR.listener, this) + } - init { - rxBus.register() - .filter { - val isIgnored = it.item == ignoreNext - if (isIgnored) ignoreNext = null - !isIgnored + // --- + + override fun refresh() = db.fetchAll() + .flattenAsFlowable { it } + .parallel() + .map { PolicyItem(it, it.applicationInfo.loadIcon(packageManager)) } + .sequential() + .sorted { o1, o2 -> + compareBy( + { it.item.appName.toLowerCase(currentLocale) }, + { it.item.packageName } + ).compare(o1, o2) + } + .toList() + .map { it to itemsPolicies.calculateDiff(it) } + .applySchedulers() + .applyViewModel(this) + .subscribeK { + itemsPolicies.update(it.first, it.second) + if (itemsPolicies.isNotEmpty()) { + itemsHelpers.remove(itemNoData) } - .subscribeK { togglePolicy(it.item, it.enable) } - .add() - rxBus.register() - .subscribeK { updatePolicy(it) } - .add() + } - updatePolicies() + // --- + + @Suppress("REDUNDANT_ELSE_IN_WHEN") + override fun onItemPressed(item: TappableHeadlineItem) = when (item) { + TappableHeadlineItem.Hide -> hidePressed() + TappableHeadlineItem.Safetynet -> safetynetPressed() + else -> Unit } - fun updatePolicies() { - if (fetchTask?.isDisposed?.not() == true) return - fetchTask = policyDB.fetchAll() - .flattenAsFlowable { it } - .map { PolicyRvItem(it, it.applicationInfo.loadIcon(packageManager)) } - .toList() - .map { - it.sortedWith(compareBy( - { it.item.appName.toLowerCase() }, - { it.item.packageName } - )) - } - .map { it to items.calculateDiff(it) } - .applySchedulers() - .applyViewModel(this) - .subscribeK { items.update(it.first, it.second) } - } + private fun safetynetPressed() = Navigation.safetynet().publish() + private fun hidePressed() = Navigation.hide().publish() - fun deletePressed(item: PolicyRvItem) { + fun deletePressed(item: PolicyItem) { fun updateState() = deletePolicy(item.item) - .map { items.filterIsInstance().toMutableList() } - .map { it.removeAll { it.item.packageName == item.item.packageName }; it } - .map { it to items.calculateDiff(it) } - .subscribeK { items.update(it.first, it.second) } - .add() - - withView { - if (BiometricHelper.isEnabled) { - BiometricHelper.authenticate(this) { updateState() } - } else { - CustomAlertDialog(this) - .setTitle(R.string.su_revoke_title) - .setMessage(getString(R.string.su_revoke_msg, item.item.appName)) - .setPositiveButton(android.R.string.yes) { _, _ -> updateState() } - .setNegativeButton(android.R.string.no, null) - .setCancelable(true) - .show() + .subscribeK { + itemsPolicies.removeAll { it.genericItemSameAs(item) } + if (itemsPolicies.isEmpty() && itemsHelpers.isEmpty()) { + itemsHelpers.add(itemNoData) + } } - } - } - - private fun updatePolicy(it: PolicyUpdateEvent) = when (it) { - is PolicyUpdateEvent.Notification -> updatePolicy(it.item) { - val textId = - if (it.notification) R.string.su_snack_notif_on else R.string.su_snack_notif_off - val text = resources.getString(textId).format(it.appName) - SnackbarEvent(text).publish() - } - is PolicyUpdateEvent.Log -> updatePolicy(it.item) { - val textId = - if (it.logging) R.string.su_snack_log_on else R.string.su_snack_log_off - val text = resources.getString(textId).format(it.appName) - SnackbarEvent(text).publish() - } - } - - private fun updatePolicy(item: MagiskPolicy, onSuccess: (MagiskPolicy) -> Unit) = - updatePolicy(item) - .subscribeK { onSuccess(it) } .add() - private fun togglePolicy(item: PolicyRvItem, enable: Boolean) { + if (BiometricHelper.isEnabled) { + BiometricDialog { + onSuccess { updateState() } + }.publish() + } else { + SuperuserRevokeDialog { + appName = item.item.appName + onSuccess { updateState() } + }.publish() + } + } + + //--- + + fun updatePolicy(it: PolicyUpdateEvent) = when (it) { + is PolicyUpdateEvent.Notification -> updatePolicy(it.item).map { + when { + it.notification -> R.string.su_snack_notif_on + else -> R.string.su_snack_notif_off + } to it.appName + } + is PolicyUpdateEvent.Log -> updatePolicy(it.item).map { + when { + it.logging -> R.string.su_snack_log_on + else -> R.string.su_snack_log_off + } to it.appName + } + }.map { resources.getString(it.first, it.second) } + .subscribeK { SnackbarEvent(it).publish() } + .add() + + fun togglePolicy(item: PolicyItem, enable: Boolean) { fun updateState() { - val app = item.item.copy(policy = if (enable) MagiskPolicy.ALLOW else MagiskPolicy.DENY) + val policy = if (enable) MagiskPolicy.ALLOW else MagiskPolicy.DENY + val app = item.item.copy(policy = policy) updatePolicy(app) .map { it.policy == MagiskPolicy.ALLOW } - .subscribeK { - val textId = if (it) R.string.su_snack_grant else R.string.su_snack_deny - val text = resources.getString(textId).format(item.item.appName) - SnackbarEvent(text).publish() - } + .map { if (it) R.string.su_snack_grant else R.string.su_snack_deny } + .map { resources.getString(it).format(item.item.appName) } + .subscribeK { SnackbarEvent(it).publish() } .add() } if (BiometricHelper.isEnabled) { - withView { - BiometricHelper.authenticate(this, onError = { - ignoreNext = item - item.isEnabled.toggle() - }) { updateState() } - } + BiometricDialog { + onSuccess { updateState() } + onFailure { item.isEnabled.toggle() } + }.publish() } else { updateState() } } + //--- + private fun updatePolicy(policy: MagiskPolicy) = - policyDB.update(policy).andThen(Single.just(policy)) + db.update(policy).andThen(Single.just(policy)) private fun deletePolicy(policy: MagiskPolicy) = - policyDB.delete(policy.uid).andThen(Single.just(policy)) + db.delete(policy.uid).andThen(Single.just(policy)) } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt deleted file mode 100644 index c9971d738..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt +++ /dev/null @@ -1,184 +0,0 @@ -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(null) - val title = KObservableField("") - val packageName = KObservableField("") - - val denyText = KObservableField(resources.getString(R.string.deny)) - val warningText = KObservableField(resources.getString(R.string.su_warning)) - - val selectedItemPosition = KObservableField(0) - - private val items = DiffObservableList(ComparableRvItem.callback) - private val itemBinding = ItemBinding.of> { binding, _, item -> - item.bind(binding) - } - - val adapter = BindingListViewAdapter>(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) - } - } - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/theme/Theme.kt b/app/src/main/java/com/topjohnwu/magisk/ui/theme/Theme.kt new file mode 100644 index 000000000..822a3db12 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/theme/Theme.kt @@ -0,0 +1,50 @@ +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 + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/theme/ThemeFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/theme/ThemeFragment.kt new file mode 100644 index 000000000..73bd2e1c4 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/theme/ThemeFragment.kt @@ -0,0 +1,22 @@ +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() { + + override val layoutRes = R.layout.fragment_theme_md2 + override val viewModel by viewModel() + + override fun consumeSystemWindowInsets(insets: Insets) = insets + + override fun onStart() { + super.onStart() + + activity.title = getString(R.string.section_theme) + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/theme/ThemeViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/theme/ThemeViewModel.kt new file mode 100644 index 000000000..af064a006 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/theme/ThemeViewModel.kt @@ -0,0 +1,24 @@ +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() + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt index c1304aaff..919abc2a5 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt @@ -1,29 +1,48 @@ package com.topjohnwu.magisk.utils +import android.animation.Animator +import android.animation.ValueAnimator +import android.graphics.Paint +import android.graphics.drawable.Drawable +import android.os.Build import android.view.View +import android.view.ViewAnimationUtils +import android.view.ViewGroup +import android.widget.ProgressBar +import android.widget.TextSwitcher import android.widget.TextView +import android.widget.ViewSwitcher import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.Toolbar +import androidx.core.animation.doOnEnd +import androidx.core.view.* import androidx.databinding.BindingAdapter import androidx.databinding.InverseBindingAdapter import androidx.databinding.InverseBindingListener import androidx.drawerlayout.widget.DrawerLayout import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager.widget.ViewPager +import com.google.android.material.button.MaterialButton +import com.google.android.material.card.MaterialCardView +import com.google.android.material.chip.Chip import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.navigation.NavigationView import com.google.android.material.textfield.TextInputLayout import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.extensions.drawableCompat import com.topjohnwu.magisk.extensions.replaceRandomWithSpecial import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.model.entity.state.IndeterminateState import io.reactivex.Observable import io.reactivex.disposables.Disposable import java.util.concurrent.TimeUnit +import kotlin.math.hypot +import kotlin.math.roundToInt @BindingAdapter("onNavigationClick") @@ -228,4 +247,237 @@ fun TextInputLayout.setErrorString(error: String) { val newError = error.let { if (it.isEmpty()) null else it } if (this.error == null && newError == null) return this.error = newError +} + +// md2 + +@BindingAdapter("onSelectClick", "onSelectReset", requireAll = false) +fun View.setOnSelectClickListener(listener: View.OnClickListener, resetTime: Long) { + + fun getHideTarget() = (parent as? ViewGroup)?.findViewWithTag(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 { + 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/DiffObservableList.kt b/app/src/main/java/com/topjohnwu/magisk/utils/DiffObservableList.kt index c7c61d968..b658d08fe 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/DiffObservableList.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/DiffObservableList.kt @@ -18,9 +18,9 @@ open class DiffObservableList( ) : AbstractList(), ObservableList { private val LIST_LOCK = Object() - private var list: MutableList = ArrayList() + protected var list: MutableList = ArrayList() private val listeners = ListChangeRegistry() - private val listCallback = ObservableListUpdateCallback() + protected val listCallback = ObservableListUpdateCallback() override val size: Int get() = list.size @@ -38,7 +38,7 @@ open class DiffObservableList( return doCalculateDiff(frozenList, newItems) } - private fun doCalculateDiff(oldItems: List, newItems: List?): DiffUtil.DiffResult { + protected fun doCalculateDiff(oldItems: List, newItems: List?): DiffUtil.DiffResult { return DiffUtil.calculateDiff(object : DiffUtil.Callback() { override fun getOldListSize() = oldItems.size diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/EndlessRecyclerScrollListener.kt b/app/src/main/java/com/topjohnwu/magisk/utils/EndlessRecyclerScrollListener.kt new file mode 100644 index 000000000..624e40a7d --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/EndlessRecyclerScrollListener.kt @@ -0,0 +1,116 @@ +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 diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/FilterableDiffObservableList.kt b/app/src/main/java/com/topjohnwu/magisk/utils/FilterableDiffObservableList.kt new file mode 100644 index 000000000..80e10f968 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/FilterableDiffObservableList.kt @@ -0,0 +1,85 @@ +package com.topjohnwu.magisk.utils + +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import java.util.* + +class FilterableDiffObservableList( + callback: Callback +) : DiffObservableList(callback) { + + var filter: ((T) -> Boolean)? = null + set(value) { + field = value + queueUpdate() + } + @Volatile + private var sublist: MutableList = 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): Boolean { + return sublist.addAll(elements) + } + + override fun addAll(index: Int, elements: Collection): 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/HideBottomViewOnScrollBehavior.kt b/app/src/main/java/com/topjohnwu/magisk/utils/HideBottomViewOnScrollBehavior.kt new file mode 100644 index 000000000..d3009f56a --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/HideBottomViewOnScrollBehavior.kt @@ -0,0 +1,110 @@ +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 : HideBottomViewOnScrollBehavior(), + HideableBehavior { + + 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 { + 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/HideTopViewOnScrollBehavior.kt b/app/src/main/java/com/topjohnwu/magisk/utils/HideTopViewOnScrollBehavior.kt new file mode 100644 index 000000000..6ce5f5af8 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/HideTopViewOnScrollBehavior.kt @@ -0,0 +1,142 @@ +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 : + CoordinatorLayout.Behavior(), + HideableBehavior { + + 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/HideableBehavior.kt b/app/src/main/java/com/topjohnwu/magisk/utils/HideableBehavior.kt new file mode 100644 index 000000000..5945472c5 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/HideableBehavior.kt @@ -0,0 +1,9 @@ +package com.topjohnwu.magisk.utils + +import android.view.View + +interface HideableBehavior { + + fun setHidden(view: V, hide: Boolean, lockState: Boolean = false) + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/KItemDecoration.kt b/app/src/main/java/com/topjohnwu/magisk/utils/KItemDecoration.kt index 0fc5c4f5c..28887ad2d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/KItemDecoration.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/KItemDecoration.kt @@ -10,6 +10,7 @@ import androidx.core.view.get import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.RecyclerView import com.topjohnwu.magisk.extensions.drawableCompat +import kotlin.math.roundToInt class KItemDecoration( private val context: Context, @@ -63,7 +64,7 @@ class KItemDecoration( .map { parent[it] } .forEach { child -> parent.getDecoratedBoundsWithMargins(child, bounds) - val bottom = bounds.bottom + Math.round(child.translationY) + val bottom = bounds.bottom + child.translationY.roundToInt() val top = bottom - drawable.intrinsicHeight drawable.setBounds(left, top, right, bottom) drawable.draw(canvas) @@ -93,7 +94,7 @@ class KItemDecoration( .map { parent[it] } .forEach { child -> parent.layoutManager!!.getDecoratedBoundsWithMargins(child, bounds) - val right = bounds.right + Math.round(child.translationX) + val right = bounds.right + child.translationX.roundToInt() val left = right - drawable.intrinsicWidth drawable.setBounds(left, top, right, bottom) drawable.draw(canvas) @@ -114,4 +115,4 @@ class KItemDecoration( } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/KObservableField.kt b/app/src/main/java/com/topjohnwu/magisk/utils/KObservableField.kt index cda0e5eb0..b9ae8f251 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/KObservableField.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/KObservableField.kt @@ -2,18 +2,30 @@ package com.topjohnwu.magisk.utils import androidx.databinding.Observable import androidx.databinding.ObservableField +import com.topjohnwu.magisk.model.observer.Observer import java.io.Serializable /** * Kotlin version of [ObservableField]. * You can define if wrapped type is Nullable or not. * You can use kotlin get/set syntax for value + * + * ## Notes + * This stays final for fuck's sake. Too many things depend on it, so you just cannot go around and + * change it randomly. Even though you think you're improving the design, you might be fucking this + * up in unimaginable ways. So DON'T TOUCH THIS. + * + * In order to have value-less observer you need - you guessed it - **a fucking [Observer]**! */ -open class KObservableField : ObservableField, Serializable { +class KObservableField : ObservableField, Serializable { var value: T - get() = get() - set(value) { set(value) } + set(value) { + if (field != value) { + field = value + notifyChange() + } + } constructor(init: T) { value = init @@ -23,8 +35,23 @@ open class KObservableField : ObservableField, Serializable { value = init } - @Suppress("UNCHECKED_CAST") + @Deprecated( + message = "Needed for data binding, use KObservableField.value syntax from code", + replaceWith = ReplaceWith("value") + ) override fun get(): T { - return super.get() as T + return value + } + + @Deprecated( + message = "Needed for data binding, use KObservableField.value = ... syntax from code", + replaceWith = ReplaceWith("value = newValue") + ) + override fun set(newValue: T) { + value = newValue + } + + override fun toString(): String { + return "KObservableField(value=$value)" } } diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/MotionRevealHelper.kt b/app/src/main/java/com/topjohnwu/magisk/utils/MotionRevealHelper.kt new file mode 100644 index 000000000..5b78818ab --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/MotionRevealHelper.kt @@ -0,0 +1,84 @@ +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 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.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) + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/PinchGestureCallback.kt b/app/src/main/java/com/topjohnwu/magisk/utils/PinchGestureCallback.kt new file mode 100644 index 000000000..f977546a3 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/PinchGestureCallback.kt @@ -0,0 +1,24 @@ +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 diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/PinchZoomTouchListener.kt b/app/src/main/java/com/topjohnwu/magisk/utils/PinchZoomTouchListener.kt new file mode 100644 index 000000000..678be9096 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/PinchZoomTouchListener.kt @@ -0,0 +1,66 @@ +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) + + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/SafetyNetHelper.kt b/app/src/main/java/com/topjohnwu/magisk/utils/SafetyNetHelper.kt deleted file mode 100644 index 68e7efd82..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/utils/SafetyNetHelper.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.topjohnwu.magisk.utils - -interface SafetyNetHelper { - - val version: Int - - fun attest() - - interface Callback { - fun onResponse(responseCode: Int) - } - - companion object { - - val RESPONSE_ERR = 0x01 - val CONNECTION_FAIL = 0x02 - - val BASIC_PASS = 0x10 - val CTS_PASS = 0x20 - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/TransitiveText.kt b/app/src/main/java/com/topjohnwu/magisk/utils/TransitiveText.kt new file mode 100644 index 000000000..9ff4efa6a --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/TransitiveText.kt @@ -0,0 +1,54 @@ +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() diff --git a/app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt b/app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt new file mode 100644 index 000000000..5f0ad783d --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt @@ -0,0 +1,280 @@ +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(null) + val title = KObservableField("") + val message = KObservableField("") + + 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("") + 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() { + 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, + 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 { 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 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt b/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt index d5b844b92..c2304f863 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt +++ b/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt @@ -3,7 +3,6 @@ package com.topjohnwu.magisk.view import android.content.Context import android.view.LayoutInflater import android.widget.TextView -import androidx.appcompat.app.AlertDialog import com.topjohnwu.magisk.R import com.topjohnwu.magisk.data.repository.StringRepository import com.topjohnwu.magisk.extensions.subscribeK @@ -34,7 +33,8 @@ object MarkDownWindow : KoinComponent { } fun show(activity: Context, title: String?, content: Single) { - 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(R.id.md_txt) content.map { @@ -45,11 +45,14 @@ object MarkDownWindow : KoinComponent { tv.setText(R.string.download_file_error) Completable.complete() }.subscribeK { - AlertDialog.Builder(activity) - .setTitle(title) - .setView(mv) - .setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() } - .show() + MagiskDialog(activity) + .applyTitle(title ?: "") + .applyView(mv) + .applyButton(MagiskDialog.ButtonType.NEGATIVE) { + titleRes = android.R.string.cancel + } + .reveal() + return@subscribeK } } } diff --git a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/CustomAlertDialog.kt b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/CustomAlertDialog.kt deleted file mode 100644 index bce39bba8..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/CustomAlertDialog.kt +++ /dev/null @@ -1,113 +0,0 @@ -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() - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/EnvFixDialog.kt b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/EnvFixDialog.kt deleted file mode 100644 index 12d8b81a2..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/EnvFixDialog.kt +++ /dev/null @@ -1,51 +0,0 @@ -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) - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/InstallMethodDialog.kt b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/InstallMethodDialog.kt deleted file mode 100644 index 6e1e812c1..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/InstallMethodDialog.kt +++ /dev/null @@ -1,73 +0,0 @@ -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) : - 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() - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/MagiskInstallDialog.kt b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/MagiskInstallDialog.kt deleted file mode 100644 index 2f83b6f98..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/MagiskInstallDialog.kt +++ /dev/null @@ -1,44 +0,0 @@ -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() - 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) - } - } - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/ManagerInstallDialog.kt b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/ManagerInstallDialog.kt deleted file mode 100644 index cea430e32..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/ManagerInstallDialog.kt +++ /dev/null @@ -1,26 +0,0 @@ -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) } - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/UninstallDialog.kt b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/UninstallDialog.kt deleted file mode 100644 index 8c7244172..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/UninstallDialog.kt +++ /dev/null @@ -1,40 +0,0 @@ -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) - } - } - } - } -} diff --git a/app/src/main/res/anim/fragment_enter.xml b/app/src/main/res/anim/fragment_enter.xml new file mode 100644 index 000000000..affbb54b9 --- /dev/null +++ b/app/src/main/res/anim/fragment_enter.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fragment_enter_pop.xml b/app/src/main/res/anim/fragment_enter_pop.xml new file mode 100644 index 000000000..6a1d9f37c --- /dev/null +++ b/app/src/main/res/anim/fragment_enter_pop.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fragment_exit.xml b/app/src/main/res/anim/fragment_exit.xml new file mode 100644 index 000000000..59ca28476 --- /dev/null +++ b/app/src/main/res/anim/fragment_exit.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fragment_exit_pop.xml b/app/src/main/res/anim/fragment_exit_pop.xml new file mode 100644 index 000000000..c39aaf6fb --- /dev/null +++ b/app/src/main/res/anim/fragment_exit_pop.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/switcher_bottom_up.xml b/app/src/main/res/anim/switcher_bottom_up.xml new file mode 100644 index 000000000..266ec3aa1 --- /dev/null +++ b/app/src/main/res/anim/switcher_bottom_up.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/switcher_center_up.xml b/app/src/main/res/anim/switcher_center_up.xml new file mode 100644 index 000000000..a19a366e9 --- /dev/null +++ b/app/src/main/res/anim/switcher_center_up.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/color_checkbox_primary.xml b/app/src/main/res/color/color_checkbox_primary.xml new file mode 100644 index 000000000..953b0d3f4 --- /dev/null +++ b/app/src/main/res/color/color_checkbox_primary.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/color_error_primary_transient.xml b/app/src/main/res/color/color_error_primary_transient.xml new file mode 100644 index 000000000..b0fddc1f3 --- /dev/null +++ b/app/src/main/res/color/color_error_primary_transient.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/color_error_transient.xml b/app/src/main/res/color/color_error_transient.xml new file mode 100644 index 000000000..22fd9491c --- /dev/null +++ b/app/src/main/res/color/color_error_transient.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/color_menu_tint.xml b/app/src/main/res/color/color_menu_tint.xml new file mode 100644 index 000000000..030b0e573 --- /dev/null +++ b/app/src/main/res/color/color_menu_tint.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/color_on_primary_transient.xml b/app/src/main/res/color/color_on_primary_transient.xml new file mode 100644 index 000000000..d82ebd18d --- /dev/null +++ b/app/src/main/res/color/color_on_primary_transient.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/color_primary_error_transient.xml b/app/src/main/res/color/color_primary_error_transient.xml new file mode 100644 index 000000000..b0bcb827a --- /dev/null +++ b/app/src/main/res/color/color_primary_error_transient.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/color_primary_transient.xml b/app/src/main/res/color/color_primary_transient.xml new file mode 100644 index 000000000..b792af898 --- /dev/null +++ b/app/src/main/res/color/color_primary_transient.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/color_secondary_transient.xml b/app/src/main/res/color/color_secondary_transient.xml new file mode 100644 index 000000000..e6ed58c86 --- /dev/null +++ b/app/src/main/res/color/color_secondary_transient.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/color_state_primary_transient.xml b/app/src/main/res/color/color_state_primary_transient.xml new file mode 100644 index 000000000..f979fd372 --- /dev/null +++ b/app/src/main/res/color/color_state_primary_transient.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/color_text_transient.xml b/app/src/main/res/color/color_text_transient.xml new file mode 100644 index 000000000..b394648cb --- /dev/null +++ b/app/src/main/res/color/color_text_transient.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/bg_appbar.xml b/app/src/main/res/drawable-v21/bg_appbar.xml new file mode 100644 index 000000000..264dc253c --- /dev/null +++ b/app/src/main/res/drawable-v21/bg_appbar.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v21/bg_card.xml b/app/src/main/res/drawable-v21/bg_card.xml new file mode 100644 index 000000000..57de59736 --- /dev/null +++ b/app/src/main/res/drawable-v21/bg_card.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/bg_divider_rounded_on_primary.xml b/app/src/main/res/drawable-v21/bg_divider_rounded_on_primary.xml new file mode 100644 index 000000000..783719e7d --- /dev/null +++ b/app/src/main/res/drawable-v21/bg_divider_rounded_on_primary.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/bg_selectable.xml b/app/src/main/res/drawable-v21/bg_selectable.xml new file mode 100644 index 000000000..3fbf0d437 --- /dev/null +++ b/app/src/main/res/drawable-v21/bg_selectable.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/bg_selectable_borderless.xml b/app/src/main/res/drawable-v21/bg_selectable_borderless.xml new file mode 100644 index 000000000..32bb31128 --- /dev/null +++ b/app/src/main/res/drawable-v21/bg_selectable_borderless.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/bg_shadow.xml b/app/src/main/res/drawable-v21/bg_shadow.xml new file mode 100644 index 000000000..4f7bd989b --- /dev/null +++ b/app/src/main/res/drawable-v21/bg_shadow.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/fast_scroll_thumb.xml b/app/src/main/res/drawable-v21/fast_scroll_thumb.xml new file mode 100644 index 000000000..19c3590fa --- /dev/null +++ b/app/src/main/res/drawable-v21/fast_scroll_thumb.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v21/progress_determinate.xml b/app/src/main/res/drawable-v21/progress_determinate.xml new file mode 100644 index 000000000..95b275b5d --- /dev/null +++ b/app/src/main/res/drawable-v21/progress_determinate.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v26/sc_cloud_download.xml b/app/src/main/res/drawable-v26/sc_cloud_download.xml deleted file mode 100644 index 3d3c9114a..000000000 --- a/app/src/main/res/drawable-v26/sc_cloud_download.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/avd_bug_from_filled.xml b/app/src/main/res/drawable/avd_bug_from_filled.xml new file mode 100644 index 000000000..ec6640d2f --- /dev/null +++ b/app/src/main/res/drawable/avd_bug_from_filled.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_bug_to_filled.xml b/app/src/main/res/drawable/avd_bug_to_filled.xml new file mode 100644 index 000000000..c42b54303 --- /dev/null +++ b/app/src/main/res/drawable/avd_bug_to_filled.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_circle_check_from_filled.xml b/app/src/main/res/drawable/avd_circle_check_from_filled.xml new file mode 100644 index 000000000..91d8e5534 --- /dev/null +++ b/app/src/main/res/drawable/avd_circle_check_from_filled.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_circle_check_to_filled.xml b/app/src/main/res/drawable/avd_circle_check_to_filled.xml new file mode 100644 index 000000000..e730bd75a --- /dev/null +++ b/app/src/main/res/drawable/avd_circle_check_to_filled.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_circle_from_filled.xml b/app/src/main/res/drawable/avd_circle_from_filled.xml new file mode 100644 index 000000000..8d4f5232f --- /dev/null +++ b/app/src/main/res/drawable/avd_circle_from_filled.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_circle_to_filled.xml b/app/src/main/res/drawable/avd_circle_to_filled.xml new file mode 100644 index 000000000..d88ac4d9f --- /dev/null +++ b/app/src/main/res/drawable/avd_circle_to_filled.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_delete_magisk.xml b/app/src/main/res/drawable/avd_delete_magisk.xml new file mode 100644 index 000000000..3929fe772 --- /dev/null +++ b/app/src/main/res/drawable/avd_delete_magisk.xml @@ -0,0 +1,387 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_home_from_filled.xml b/app/src/main/res/drawable/avd_home_from_filled.xml new file mode 100644 index 000000000..c8bb9cc30 --- /dev/null +++ b/app/src/main/res/drawable/avd_home_from_filled.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_home_to_filled.xml b/app/src/main/res/drawable/avd_home_to_filled.xml new file mode 100644 index 000000000..6078b8137 --- /dev/null +++ b/app/src/main/res/drawable/avd_home_to_filled.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_magisk_delete.xml b/app/src/main/res/drawable/avd_magisk_delete.xml new file mode 100644 index 000000000..de6975b99 --- /dev/null +++ b/app/src/main/res/drawable/avd_magisk_delete.xml @@ -0,0 +1,428 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_module_from_filled.xml b/app/src/main/res/drawable/avd_module_from_filled.xml new file mode 100644 index 000000000..dadb042b0 --- /dev/null +++ b/app/src/main/res/drawable/avd_module_from_filled.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_module_to_filled.xml b/app/src/main/res/drawable/avd_module_to_filled.xml new file mode 100644 index 000000000..cc4107675 --- /dev/null +++ b/app/src/main/res/drawable/avd_module_to_filled.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_settings_from_filled.xml b/app/src/main/res/drawable/avd_settings_from_filled.xml new file mode 100644 index 000000000..beb981c52 --- /dev/null +++ b/app/src/main/res/drawable/avd_settings_from_filled.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_settings_to_filled.xml b/app/src/main/res/drawable/avd_settings_to_filled.xml new file mode 100644 index 000000000..00d8ed2da --- /dev/null +++ b/app/src/main/res/drawable/avd_settings_to_filled.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_superuser_from_filled.xml b/app/src/main/res/drawable/avd_superuser_from_filled.xml new file mode 100644 index 000000000..99f19734d --- /dev/null +++ b/app/src/main/res/drawable/avd_superuser_from_filled.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/avd_superuser_to_filled.xml b/app/src/main/res/drawable/avd_superuser_to_filled.xml new file mode 100644 index 000000000..a105e0f00 --- /dev/null +++ b/app/src/main/res/drawable/avd_superuser_to_filled.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_appbar.xml b/app/src/main/res/drawable/bg_appbar.xml new file mode 100644 index 000000000..9e8232951 --- /dev/null +++ b/app/src/main/res/drawable/bg_appbar.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_card.xml b/app/src/main/res/drawable/bg_card.xml new file mode 100644 index 000000000..66b0213d7 --- /dev/null +++ b/app/src/main/res/drawable/bg_card.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/bg_divider_rounded_on_primary.xml b/app/src/main/res/drawable/bg_divider_rounded_on_primary.xml new file mode 100644 index 000000000..a33cc5fee --- /dev/null +++ b/app/src/main/res/drawable/bg_divider_rounded_on_primary.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_line_bottom_rounded.xml b/app/src/main/res/drawable/bg_line_bottom_rounded.xml new file mode 100644 index 000000000..7ead075af --- /dev/null +++ b/app/src/main/res/drawable/bg_line_bottom_rounded.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_line_top_rounded.xml b/app/src/main/res/drawable/bg_line_top_rounded.xml new file mode 100644 index 000000000..589b19ca4 --- /dev/null +++ b/app/src/main/res/drawable/bg_line_top_rounded.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_selectable.xml b/app/src/main/res/drawable/bg_selectable.xml new file mode 100644 index 000000000..b626ec142 --- /dev/null +++ b/app/src/main/res/drawable/bg_selectable.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_selectable_borderless.xml b/app/src/main/res/drawable/bg_selectable_borderless.xml new file mode 100644 index 000000000..b626ec142 --- /dev/null +++ b/app/src/main/res/drawable/bg_selectable_borderless.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_selection_circle_green.xml b/app/src/main/res/drawable/bg_selection_circle_green.xml new file mode 100644 index 000000000..38788d14a --- /dev/null +++ b/app/src/main/res/drawable/bg_selection_circle_green.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_shadow.xml b/app/src/main/res/drawable/bg_shadow.xml new file mode 100644 index 000000000..5d6c86bcf --- /dev/null +++ b/app/src/main/res/drawable/bg_shadow.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/divider_l1.xml b/app/src/main/res/drawable/divider_l1.xml new file mode 100644 index 000000000..ec3c9944d --- /dev/null +++ b/app/src/main/res/drawable/divider_l1.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/divider_l_50.xml b/app/src/main/res/drawable/divider_l_50.xml new file mode 100644 index 000000000..51b4e1496 --- /dev/null +++ b/app/src/main/res/drawable/divider_l_50.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fast_scroll_thumb.xml b/app/src/main/res/drawable/fast_scroll_thumb.xml new file mode 100644 index 000000000..f0ae9cd31 --- /dev/null +++ b/app/src/main/res/drawable/fast_scroll_thumb.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/fast_scroll_track.xml b/app/src/main/res/drawable/fast_scroll_track.xml new file mode 100644 index 000000000..c8b91c814 --- /dev/null +++ b/app/src/main/res/drawable/fast_scroll_track.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml deleted file mode 100644 index 0258249cc..000000000 --- a/app/src/main/res/drawable/ic_add.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow.xml b/app/src/main/res/drawable/ic_arrow.xml deleted file mode 100644 index 46fd51124..000000000 --- a/app/src/main/res/drawable/ic_arrow.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_back_md2.xml b/app/src/main/res/drawable/ic_back_md2.xml new file mode 100644 index 000000000..2342f12e4 --- /dev/null +++ b/app/src/main/res/drawable/ic_back_md2.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_bug_report.xml b/app/src/main/res/drawable/ic_bug_filled_md2.xml similarity index 70% rename from app/src/main/res/drawable/ic_bug_report.xml rename to app/src/main/res/drawable/ic_bug_filled_md2.xml index 0ac39ddc1..6849c1b2a 100644 --- a/app/src/main/res/drawable/ic_bug_report.xml +++ b/app/src/main/res/drawable/ic_bug_filled_md2.xml @@ -1,9 +1,10 @@ + + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + 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" /> diff --git a/app/src/main/res/drawable/ic_bug_md2.xml b/app/src/main/res/drawable/ic_bug_md2.xml new file mode 100644 index 000000000..e9ea123fc --- /dev/null +++ b/app/src/main/res/drawable/ic_bug_md2.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_bug_outlined_md2.xml b/app/src/main/res/drawable/ic_bug_outlined_md2.xml new file mode 100644 index 000000000..2c9a0a795 --- /dev/null +++ b/app/src/main/res/drawable/ic_bug_outlined_md2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cancel.xml b/app/src/main/res/drawable/ic_cancel.xml deleted file mode 100644 index e6545bf8a..000000000 --- a/app/src/main/res/drawable/ic_cancel.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml deleted file mode 100644 index 45d1b3076..000000000 --- a/app/src/main/res/drawable/ic_check_circle.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_check_circle_checked_md2.xml b/app/src/main/res/drawable/ic_check_circle_checked_md2.xml new file mode 100644 index 000000000..926f8e6ae --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle_checked_md2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_check_circle_md2.xml b/app/src/main/res/drawable/ic_check_circle_md2.xml new file mode 100644 index 000000000..09f36eefc --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle_md2.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_check_circle_unchecked_md2.xml b/app/src/main/res/drawable/ic_check_circle_unchecked_md2.xml new file mode 100644 index 000000000..1fae2758e --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle_unchecked_md2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_check_md2.xml b/app/src/main/res/drawable/ic_check_md2.xml new file mode 100644 index 000000000..ff6645704 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_md2.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_close_md2.xml b/app/src/main/res/drawable/ic_close_md2.xml new file mode 100644 index 000000000..786303ae0 --- /dev/null +++ b/app/src/main/res/drawable/ic_close_md2.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_cloud_download.xml b/app/src/main/res/drawable/ic_cloud_download.xml deleted file mode 100644 index a198cf0a1..000000000 --- a/app/src/main/res/drawable/ic_cloud_download.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_day.xml b/app/src/main/res/drawable/ic_day.xml new file mode 100644 index 000000000..2899017da --- /dev/null +++ b/app/src/main/res/drawable/ic_day.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_day_night.xml b/app/src/main/res/drawable/ic_day_night.xml new file mode 100644 index 000000000..8452fab77 --- /dev/null +++ b/app/src/main/res/drawable/ic_day_night.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_delete_md2.xml b/app/src/main/res/drawable/ic_delete_md2.xml new file mode 100644 index 000000000..e451bfa33 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_md2.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_device.xml b/app/src/main/res/drawable/ic_device.xml new file mode 100644 index 000000000..5b90a6084 --- /dev/null +++ b/app/src/main/res/drawable/ic_device.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_download_md2.xml b/app/src/main/res/drawable/ic_download_md2.xml new file mode 100644 index 000000000..5d3c38484 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_md2.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_file_download_black.xml b/app/src/main/res/drawable/ic_file_download_black.xml deleted file mode 100644 index d05655222..000000000 --- a/app/src/main/res/drawable/ic_file_download_black.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml new file mode 100644 index 000000000..f327b4a79 --- /dev/null +++ b/app/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_folder_list.xml b/app/src/main/res/drawable/ic_folder_list.xml new file mode 100644 index 000000000..7c13a421c --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_list.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_forth_md2.xml b/app/src/main/res/drawable/ic_forth_md2.xml new file mode 100644 index 000000000..261bcc7b3 --- /dev/null +++ b/app/src/main/res/drawable/ic_forth_md2.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_help.xml b/app/src/main/res/drawable/ic_help.xml deleted file mode 100644 index 98d384f8b..000000000 --- a/app/src/main/res/drawable/ic_help.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_hide_md2.xml b/app/src/main/res/drawable/ic_hide_md2.xml new file mode 100644 index 000000000..1930df665 --- /dev/null +++ b/app/src/main/res/drawable/ic_hide_md2.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_hide_select_md2.xml b/app/src/main/res/drawable/ic_hide_select_md2.xml new file mode 100644 index 000000000..cb83a097c --- /dev/null +++ b/app/src/main/res/drawable/ic_hide_select_md2.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_home_filled_md2.xml b/app/src/main/res/drawable/ic_home_filled_md2.xml new file mode 100644 index 000000000..0dafe8a4a --- /dev/null +++ b/app/src/main/res/drawable/ic_home_filled_md2.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_md2.xml b/app/src/main/res/drawable/ic_home_md2.xml new file mode 100644 index 000000000..a5d90b4d9 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_md2.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_home_outlined_md2.xml b/app/src/main/res/drawable/ic_home_outlined_md2.xml new file mode 100644 index 000000000..38bad7cc5 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_outlined_md2.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 000000000..ab24089b6 --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_install.xml b/app/src/main/res/drawable/ic_install.xml new file mode 100644 index 000000000..9e71aa185 --- /dev/null +++ b/app/src/main/res/drawable/ic_install.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_magisk_delete.xml b/app/src/main/res/drawable/ic_magisk_delete.xml new file mode 100644 index 000000000..3feade979 --- /dev/null +++ b/app/src/main/res/drawable/ic_magisk_delete.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_manager.xml b/app/src/main/res/drawable/ic_manager.xml new file mode 100644 index 000000000..001df45e3 --- /dev/null +++ b/app/src/main/res/drawable/ic_manager.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_module_filled_md2.xml b/app/src/main/res/drawable/ic_module_filled_md2.xml new file mode 100644 index 000000000..8d4c14e57 --- /dev/null +++ b/app/src/main/res/drawable/ic_module_filled_md2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_module_md2.xml b/app/src/main/res/drawable/ic_module_md2.xml new file mode 100644 index 000000000..d93278f5b --- /dev/null +++ b/app/src/main/res/drawable/ic_module_md2.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_module_outlined_md2.xml b/app/src/main/res/drawable/ic_module_outlined_md2.xml new file mode 100644 index 000000000..fd83b65c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_module_outlined_md2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_module_storage_md2.xml b/app/src/main/res/drawable/ic_module_storage_md2.xml new file mode 100644 index 000000000..9505cddde --- /dev/null +++ b/app/src/main/res/drawable/ic_module_storage_md2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_night.xml b/app/src/main/res/drawable/ic_night.xml new file mode 100644 index 000000000..c31bd0593 --- /dev/null +++ b/app/src/main/res/drawable/ic_night.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notifications.xml b/app/src/main/res/drawable/ic_notifications.xml deleted file mode 100644 index be9f8368d..000000000 --- a/app/src/main/res/drawable/ic_notifications.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_notifications_md2.xml b/app/src/main/res/drawable/ic_notifications_md2.xml new file mode 100644 index 000000000..c55141254 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_md2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_off.xml b/app/src/main/res/drawable/ic_off.xml new file mode 100644 index 000000000..377067b0d --- /dev/null +++ b/app/src/main/res/drawable/ic_off.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_order_date.xml b/app/src/main/res/drawable/ic_order_date.xml new file mode 100644 index 000000000..f98e20ddd --- /dev/null +++ b/app/src/main/res/drawable/ic_order_date.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_order_name.xml b/app/src/main/res/drawable/ic_order_name.xml new file mode 100644 index 000000000..a845aa351 --- /dev/null +++ b/app/src/main/res/drawable/ic_order_name.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_paint.xml b/app/src/main/res/drawable/ic_paint.xml new file mode 100644 index 000000000..9917845ce --- /dev/null +++ b/app/src/main/res/drawable/ic_paint.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_radio_check_button.xml b/app/src/main/res/drawable/ic_radio_check_button.xml new file mode 100644 index 000000000..96cc1e9bd --- /dev/null +++ b/app/src/main/res/drawable/ic_radio_check_button.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_refresh_data_md2.xml b/app/src/main/res/drawable/ic_refresh_data_md2.xml new file mode 100644 index 000000000..5eb26cd74 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_data_md2.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_refresh_safetynet_md2.xml b/app/src/main/res/drawable/ic_refresh_safetynet_md2.xml new file mode 100644 index 000000000..b5285029e --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_safetynet_md2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_safetynet.xml b/app/src/main/res/drawable/ic_safetynet.xml deleted file mode 100644 index f47a5deae..000000000 --- a/app/src/main/res/drawable/ic_safetynet.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_safetynet_md2.xml b/app/src/main/res/drawable/ic_safetynet_md2.xml new file mode 100644 index 000000000..dec5d71b6 --- /dev/null +++ b/app/src/main/res/drawable/ic_safetynet_md2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_save.xml b/app/src/main/res/drawable/ic_save.xml deleted file mode 100644 index fed1b783b..000000000 --- a/app/src/main/res/drawable/ic_save.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_save_md2.xml b/app/src/main/res/drawable/ic_save_md2.xml new file mode 100644 index 000000000..81d0e3b04 --- /dev/null +++ b/app/src/main/res/drawable/ic_save_md2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_search_md2.xml b/app/src/main/res/drawable/ic_search_md2.xml new file mode 100644 index 000000000..626a6c3ce --- /dev/null +++ b/app/src/main/res/drawable/ic_search_md2.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml deleted file mode 100644 index ace746c40..000000000 --- a/app/src/main/res/drawable/ic_settings.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_settings_filled_md2.xml b/app/src/main/res/drawable/ic_settings_filled_md2.xml new file mode 100644 index 000000000..6044b0f0f --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_filled_md2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_md2.xml b/app/src/main/res/drawable/ic_settings_md2.xml new file mode 100644 index 000000000..9d294bff8 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_md2.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_outlined_md2.xml b/app/src/main/res/drawable/ic_settings_outlined_md2.xml new file mode 100644 index 000000000..6c20efa2f --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_outlined_md2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_show_md2.xml b/app/src/main/res/drawable/ic_show_md2.xml new file mode 100644 index 000000000..76e8c2ae0 --- /dev/null +++ b/app/src/main/res/drawable/ic_show_md2.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_sort.xml b/app/src/main/res/drawable/ic_sort.xml deleted file mode 100644 index c872f6bd2..000000000 --- a/app/src/main/res/drawable/ic_sort.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_menu.xml b/app/src/main/res/drawable/ic_superuser_filled_md2.xml similarity index 57% rename from app/src/main/res/drawable/ic_menu.xml rename to app/src/main/res/drawable/ic_superuser_filled_md2.xml index d1bac8735..dde6afed0 100644 --- a/app/src/main/res/drawable/ic_menu.xml +++ b/app/src/main/res/drawable/ic_superuser_filled_md2.xml @@ -1,10 +1,9 @@ - + android:fillColor="?colorOnSurface" + android:pathData="M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1Z" /> diff --git a/app/src/main/res/drawable/ic_superuser_md2.xml b/app/src/main/res/drawable/ic_superuser_md2.xml new file mode 100644 index 000000000..374959c7c --- /dev/null +++ b/app/src/main/res/drawable/ic_superuser_md2.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_superuser_outlined_md2.xml b/app/src/main/res/drawable/ic_superuser_outlined_md2.xml new file mode 100644 index 000000000..3c279ccbe --- /dev/null +++ b/app/src/main/res/drawable/ic_superuser_outlined_md2.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_undelete.xml b/app/src/main/res/drawable/ic_undelete.xml deleted file mode 100644 index 8546fea72..000000000 --- a/app/src/main/res/drawable/ic_undelete.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_up_md2.xml b/app/src/main/res/drawable/ic_up_md2.xml new file mode 100644 index 000000000..607a43c89 --- /dev/null +++ b/app/src/main/res/drawable/ic_up_md2.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_update.xml b/app/src/main/res/drawable/ic_update.xml deleted file mode 100644 index 91a4d5abf..000000000 --- a/app/src/main/res/drawable/ic_update.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_down.xml b/app/src/main/res/drawable/ic_update_md2.xml similarity index 54% rename from app/src/main/res/drawable/ic_arrow_down.xml rename to app/src/main/res/drawable/ic_update_md2.xml index 569d3a745..7595bcf4d 100644 --- a/app/src/main/res/drawable/ic_arrow_down.xml +++ b/app/src/main/res/drawable/ic_update_md2.xml @@ -5,6 +5,6 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:fillColor="?colorOnSurface" + android:pathData="M17,1H7A2,2 0 0,0 5,3V21A2,2 0 0,0 7,23H17A2,2 0 0,0 19,21V3A2,2 0 0,0 17,1M17,19H7V5H17V19M16,13H13V8H11V13H8L12,17L16,13Z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_warning.xml b/app/src/main/res/drawable/ic_warning.xml deleted file mode 100644 index 87e5f4ac4..000000000 --- a/app/src/main/res/drawable/ic_warning.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/progress_determinate.xml b/app/src/main/res/drawable/progress_determinate.xml new file mode 100644 index 000000000..c50466498 --- /dev/null +++ b/app/src/main/res/drawable/progress_determinate.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/sc_cloud_download.xml b/app/src/main/res/drawable/sc_cloud_download.xml deleted file mode 100644 index 1a7dae048..000000000 --- a/app/src/main/res/drawable/sc_cloud_download.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/font/exo.xml b/app/src/main/res/font/exo.xml new file mode 100644 index 000000000..e58a4ba9c --- /dev/null +++ b/app/src/main/res/font/exo.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/font/exo_bold.ttf b/app/src/main/res/font/exo_bold.ttf new file mode 100644 index 000000000..8ea37706c Binary files /dev/null and b/app/src/main/res/font/exo_bold.ttf differ diff --git a/app/src/main/res/font/exo_bold_italic.ttf b/app/src/main/res/font/exo_bold_italic.ttf new file mode 100644 index 000000000..7ec56c94e Binary files /dev/null and b/app/src/main/res/font/exo_bold_italic.ttf differ diff --git a/app/src/main/res/font/exo_regular.ttf b/app/src/main/res/font/exo_regular.ttf new file mode 100644 index 000000000..cf681a354 Binary files /dev/null and b/app/src/main/res/font/exo_regular.ttf differ diff --git a/app/src/main/res/font/exo_regular_italic.ttf b/app/src/main/res/font/exo_regular_italic.ttf new file mode 100644 index 000000000..5673cabab Binary files /dev/null and b/app/src/main/res/font/exo_regular_italic.ttf differ diff --git a/app/src/main/res/layout/activity_flash.xml b/app/src/main/res/layout/activity_flash.xml index 81dc153f7..cecf04191 100644 --- a/app/src/main/res/layout/activity_flash.xml +++ b/app/src/main/res/layout/activity_flash.xml @@ -7,7 +7,7 @@ + type="com.topjohnwu.magisk.legacy.flash.FlashViewModel" /> @@ -149,4 +149,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index f2300aafb..000000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_main_content.xml b/app/src/main/res/layout/activity_main_content.xml deleted file mode 100644 index ab5775c0d..000000000 --- a/app/src/main/res/layout/activity_main_content.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_main_md2.xml b/app/src/main/res/layout/activity_main_md2.xml new file mode 100644 index 000000000..588b460b5 --- /dev/null +++ b/app/src/main/res/layout/activity_main_md2.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_request.xml b/app/src/main/res/layout/activity_request.xml index 70af73e7d..6d0ed9652 100644 --- a/app/src/main/res/layout/activity_request.xml +++ b/app/src/main/res/layout/activity_request.xml @@ -7,7 +7,7 @@ + type="com.topjohnwu.magisk.legacy.surequest.SuRequestViewModel" /> diff --git a/app/src/main/res/layout/activity_request_md2.xml b/app/src/main/res/layout/activity_request_md2.xml new file mode 100644 index 000000000..2ac22e2cd --- /dev/null +++ b/app/src/main/res/layout/activity_request_md2.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/alert_dialog.xml b/app/src/main/res/layout/alert_dialog.xml deleted file mode 100644 index 3a9ef0268..000000000 --- a/app/src/main/res/layout/alert_dialog.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/custom_channel_dialog.xml b/app/src/main/res/layout/custom_channel_dialog.xml deleted file mode 100644 index 95aab1637..000000000 --- a/app/src/main/res/layout/custom_channel_dialog.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_custom_name.xml b/app/src/main/res/layout/dialog_custom_name.xml deleted file mode 100644 index 45d4ffafd..000000000 --- a/app/src/main/res/layout/dialog_custom_name.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_magisk_base.xml b/app/src/main/res/layout/dialog_magisk_base.xml new file mode 100644 index 000000000..852e0eb31 --- /dev/null +++ b/app/src/main/res/layout/dialog_magisk_base.xml @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_settings_app_name.xml b/app/src/main/res/layout/dialog_settings_app_name.xml new file mode 100644 index 000000000..cc549a77c --- /dev/null +++ b/app/src/main/res/layout/dialog_settings_app_name.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/custom_download_dialog.xml b/app/src/main/res/layout/dialog_settings_download_path.xml similarity index 71% rename from app/src/main/res/layout/custom_download_dialog.xml rename to app/src/main/res/layout/dialog_settings_download_path.xml index f182f8ff0..f3ea581a1 100644 --- a/app/src/main/res/layout/custom_download_dialog.xml +++ b/app/src/main/res/layout/dialog_settings_download_path.xml @@ -7,7 +7,7 @@ + type="com.topjohnwu.magisk.ui.settings.DownloadPath" /> @@ -22,6 +22,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{@string/settings_download_path_message(data.path)}" + android:textAppearance="@style/AppearanceFoundation.Caption" tools:text="@string/settings_download_path_message" /> + app:boxStrokeColor="?colorOnSurfaceVariant" + app:errorTextColor="?colorError" + app:hintEnabled="true" + app:hintTextAppearance="@style/AppearanceFoundation.Tiny" + app:hintTextColor="?colorOnSurfaceVariant"> + android:text="@={data.result}" + android:textAppearance="@style/AppearanceFoundation.Body" + android:textColor="?colorOnSurface" + tools:text="@tools:sample/lorem" /> diff --git a/app/src/main/res/layout/dialog_settings_update_channel.xml b/app/src/main/res/layout/dialog_settings_update_channel.xml new file mode 100644 index 000000000..26095efa2 --- /dev/null +++ b/app/src/main/res/layout/dialog_settings_update_channel.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_flash_md2.xml b/app/src/main/res/layout/fragment_flash_md2.xml new file mode 100644 index 000000000..55900f5c7 --- /dev/null +++ b/app/src/main/res/layout/fragment_flash_md2.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_hide_md2.xml b/app/src/main/res/layout/fragment_hide_md2.xml new file mode 100644 index 000000000..c425ff08d --- /dev/null +++ b/app/src/main/res/layout/fragment_hide_md2.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_home_md2.xml b/app/src/main/res/layout/fragment_home_md2.xml new file mode 100644 index 000000000..5ec0fc54a --- /dev/null +++ b/app/src/main/res/layout/fragment_home_md2.xml @@ -0,0 +1,820 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_install_md2.xml b/app/src/main/res/layout/fragment_install_md2.xml new file mode 100644 index 000000000..0c50d2fa6 --- /dev/null +++ b/app/src/main/res/layout/fragment_install_md2.xml @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_log.xml b/app/src/main/res/layout/fragment_log.xml deleted file mode 100644 index a07621cc6..000000000 --- a/app/src/main/res/layout/fragment_log.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_log_md2.xml b/app/src/main/res/layout/fragment_log_md2.xml new file mode 100644 index 000000000..daebc548c --- /dev/null +++ b/app/src/main/res/layout/fragment_log_md2.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_magisk.xml b/app/src/main/res/layout/fragment_magisk.xml deleted file mode 100644 index 70b580dca..000000000 --- a/app/src/main/res/layout/fragment_magisk.xml +++ /dev/null @@ -1,503 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_magisk_hide.xml b/app/src/main/res/layout/fragment_magisk_hide.xml deleted file mode 100644 index 81b380f88..000000000 --- a/app/src/main/res/layout/fragment_magisk_hide.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_module_md2.xml b/app/src/main/res/layout/fragment_module_md2.xml new file mode 100644 index 000000000..73ea22de0 --- /dev/null +++ b/app/src/main/res/layout/fragment_module_md2.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_modules.xml b/app/src/main/res/layout/fragment_modules.xml deleted file mode 100644 index 7439ca496..000000000 --- a/app/src/main/res/layout/fragment_modules.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_repos.xml b/app/src/main/res/layout/fragment_repos.xml deleted file mode 100644 index f1c605ae8..000000000 --- a/app/src/main/res/layout/fragment_repos.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_safetynet_md2.xml b/app/src/main/res/layout/fragment_safetynet_md2.xml new file mode 100644 index 000000000..38a456def --- /dev/null +++ b/app/src/main/res/layout/fragment_safetynet_md2.xml @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_settings_md2.xml b/app/src/main/res/layout/fragment_settings_md2.xml new file mode 100644 index 000000000..85a6fab75 --- /dev/null +++ b/app/src/main/res/layout/fragment_settings_md2.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_superuser.xml b/app/src/main/res/layout/fragment_superuser.xml deleted file mode 100644 index c67588746..000000000 --- a/app/src/main/res/layout/fragment_superuser.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_superuser_md2.xml b/app/src/main/res/layout/fragment_superuser_md2.xml new file mode 100644 index 000000000..8d4b7c32d --- /dev/null +++ b/app/src/main/res/layout/fragment_superuser_md2.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_theme_md2.xml b/app/src/main/res/layout/fragment_theme_md2.xml new file mode 100644 index 000000000..f728b03b7 --- /dev/null +++ b/app/src/main/res/layout/fragment_theme_md2.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/include_hide_filter.xml b/app/src/main/res/layout/include_hide_filter.xml new file mode 100644 index 000000000..705ca9d2c --- /dev/null +++ b/app/src/main/res/layout/include_hide_filter.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/include_install_options.xml b/app/src/main/res/layout/include_install_options.xml new file mode 100644 index 000000000..48978cded --- /dev/null +++ b/app/src/main/res/layout/include_install_options.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/include_log_magisk.xml b/app/src/main/res/layout/include_log_magisk.xml new file mode 100644 index 000000000..324f9906b --- /dev/null +++ b/app/src/main/res/layout/include_log_magisk.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/include_module_filter.xml b/app/src/main/res/layout/include_module_filter.xml new file mode 100644 index 000000000..080e9fe8f --- /dev/null +++ b/app/src/main/res/layout/include_module_filter.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/include_update_card.xml b/app/src/main/res/layout/include_update_card.xml deleted file mode 100644 index cc4bb272f..000000000 --- a/app/src/main/res/layout/include_update_card.xml +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_console_md2.xml b/app/src/main/res/layout/item_console_md2.xml new file mode 100644 index 000000000..4db78d28f --- /dev/null +++ b/app/src/main/res/layout/item_console_md2.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_developer.xml b/app/src/main/res/layout/item_developer.xml new file mode 100644 index 000000000..96279abca --- /dev/null +++ b/app/src/main/res/layout/item_developer.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_developer_link.xml b/app/src/main/res/layout/item_developer_link.xml new file mode 100644 index 000000000..41c6cee0a --- /dev/null +++ b/app/src/main/res/layout/item_developer_link.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_hide_app.xml b/app/src/main/res/layout/item_hide_app.xml deleted file mode 100644 index a3512e113..000000000 --- a/app/src/main/res/layout/item_hide_app.xml +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_hide_md2.xml b/app/src/main/res/layout/item_hide_md2.xml new file mode 100644 index 000000000..8ae1fc828 --- /dev/null +++ b/app/src/main/res/layout/item_hide_md2.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_hide_process.xml b/app/src/main/res/layout/item_hide_process_md2.xml similarity index 57% rename from app/src/main/res/layout/item_hide_process.xml rename to app/src/main/res/layout/item_hide_process_md2.xml index d0dfd0597..bd0152371 100644 --- a/app/src/main/res/layout/item_hide_process.xml +++ b/app/src/main/res/layout/item_hide_process_md2.xml @@ -7,7 +7,7 @@ + type="com.topjohnwu.magisk.model.entity.recycler.HideProcessItem" /> + android:onClick="@{() -> item.toggle(viewModel)}" + android:layout_height="wrap_content" + android:layout_gravity="center"> + tools:text="com.topjohnwu.magisk" /> + app:srcCompat="@drawable/ic_radio_check_button" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/item_list_single_line.xml b/app/src/main/res/layout/item_list_single_line.xml new file mode 100644 index 000000000..e0f12d569 --- /dev/null +++ b/app/src/main/res/layout/item_list_single_line.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_log_access_md2.xml b/app/src/main/res/layout/item_log_access_md2.xml new file mode 100644 index 000000000..5a3472f12 --- /dev/null +++ b/app/src/main/res/layout/item_log_access_md2.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_log_track_md2.xml b/app/src/main/res/layout/item_log_track_md2.xml new file mode 100644 index 000000000..4f4d2d182 --- /dev/null +++ b/app/src/main/res/layout/item_log_track_md2.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_module.xml b/app/src/main/res/layout/item_module.xml deleted file mode 100644 index 51b46f9a0..000000000 --- a/app/src/main/res/layout/item_module.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_module_download.xml b/app/src/main/res/layout/item_module_download.xml new file mode 100644 index 000000000..6f6b4fa4e --- /dev/null +++ b/app/src/main/res/layout/item_module_download.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_module_md2.xml b/app/src/main/res/layout/item_module_md2.xml new file mode 100644 index 000000000..9db5ce78b --- /dev/null +++ b/app/src/main/res/layout/item_module_md2.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_page_log.xml b/app/src/main/res/layout/item_page_log.xml deleted file mode 100644 index eff2e06b5..000000000 --- a/app/src/main/res/layout/item_page_log.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_page_magisk_log.xml b/app/src/main/res/layout/item_page_magisk_log.xml deleted file mode 100644 index b2f4f8c91..000000000 --- a/app/src/main/res/layout/item_page_magisk_log.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_policy.xml b/app/src/main/res/layout/item_policy.xml deleted file mode 100644 index e076a6538..000000000 --- a/app/src/main/res/layout/item_policy.xml +++ /dev/null @@ -1,211 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_policy_md2.xml b/app/src/main/res/layout/item_policy_md2.xml new file mode 100644 index 000000000..0383e8684 --- /dev/null +++ b/app/src/main/res/layout/item_policy_md2.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_repo.xml b/app/src/main/res/layout/item_repo.xml deleted file mode 100644 index efada5183..000000000 --- a/app/src/main/res/layout/item_repo.xml +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_repo_md2.xml b/app/src/main/res/layout/item_repo_md2.xml new file mode 100644 index 000000000..7b05e031a --- /dev/null +++ b/app/src/main/res/layout/item_repo_md2.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_safe_mode_notice.xml b/app/src/main/res/layout/item_safe_mode_notice.xml new file mode 100644 index 000000000..f99e806f4 --- /dev/null +++ b/app/src/main/res/layout/item_safe_mode_notice.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_section_md2.xml b/app/src/main/res/layout/item_section_md2.xml new file mode 100644 index 000000000..65568bda5 --- /dev/null +++ b/app/src/main/res/layout/item_section_md2.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_settings_blank.xml b/app/src/main/res/layout/item_settings_blank.xml new file mode 100644 index 000000000..31de4e803 --- /dev/null +++ b/app/src/main/res/layout/item_settings_blank.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_settings_input.xml b/app/src/main/res/layout/item_settings_input.xml new file mode 100644 index 000000000..0eb5fac97 --- /dev/null +++ b/app/src/main/res/layout/item_settings_input.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_settings_section.xml b/app/src/main/res/layout/item_settings_section.xml new file mode 100644 index 000000000..92e751ee6 --- /dev/null +++ b/app/src/main/res/layout/item_settings_section.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_settings_selector.xml b/app/src/main/res/layout/item_settings_selector.xml new file mode 100644 index 000000000..edebf5b91 --- /dev/null +++ b/app/src/main/res/layout/item_settings_selector.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_settings_toggle.xml b/app/src/main/res/layout/item_settings_toggle.xml new file mode 100644 index 000000000..14b6e3749 --- /dev/null +++ b/app/src/main/res/layout/item_settings_toggle.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_superuser_log.xml b/app/src/main/res/layout/item_superuser_log.xml deleted file mode 100644 index 332d1d30f..000000000 --- a/app/src/main/res/layout/item_superuser_log.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_superuser_log_entry.xml b/app/src/main/res/layout/item_superuser_log_entry.xml deleted file mode 100644 index da660d8fc..000000000 --- a/app/src/main/res/layout/item_superuser_log_entry.xml +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_tappable_headline.xml b/app/src/main/res/layout/item_tappable_headline.xml new file mode 100644 index 000000000..a260c2cac --- /dev/null +++ b/app/src/main/res/layout/item_tappable_headline.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_text.xml b/app/src/main/res/layout/item_text.xml new file mode 100644 index 000000000..7bf74922f --- /dev/null +++ b/app/src/main/res/layout/item_text.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_theme.xml b/app/src/main/res/layout/item_theme.xml new file mode 100644 index 000000000..97c030bb5 --- /dev/null +++ b/app/src/main/res/layout/item_theme.xml @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/markdown_window.xml b/app/src/main/res/layout/markdown_window_md2.xml similarity index 80% rename from app/src/main/res/layout/markdown_window.xml rename to app/src/main/res/layout/markdown_window_md2.xml index f5b3d43be..f0fd7ac4f 100644 --- a/app/src/main/res/layout/markdown_window.xml +++ b/app/src/main/res/layout/markdown_window_md2.xml @@ -9,6 +9,7 @@ android:layout_height="wrap_content" android:layout_marginStart="15dp" android:layout_marginEnd="15dp" - android:paddingTop="10dp" /> + android:paddingTop="10dp" + android:textAppearance="@style/AppearanceFoundation.Caption" /> \ No newline at end of file diff --git a/app/src/main/res/layout/swicher_caption_variant.xml b/app/src/main/res/layout/swicher_caption_variant.xml new file mode 100644 index 000000000..02c1d64a7 --- /dev/null +++ b/app/src/main/res/layout/swicher_caption_variant.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/drawer.xml b/app/src/main/res/menu/drawer.xml deleted file mode 100644 index 6710d2ddc..000000000 --- a/app/src/main/res/menu/drawer.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/menu_bottom_nav.xml b/app/src/main/res/menu/menu_bottom_nav.xml new file mode 100644 index 000000000..dd797ea46 --- /dev/null +++ b/app/src/main/res/menu/menu_bottom_nav.xml @@ -0,0 +1,23 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_hide_md2.xml b/app/src/main/res/menu/menu_hide_md2.xml new file mode 100644 index 000000000..8b4ab146d --- /dev/null +++ b/app/src/main/res/menu/menu_hide_md2.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_home_md2.xml b/app/src/main/res/menu/menu_home_md2.xml new file mode 100644 index 000000000..0825147da --- /dev/null +++ b/app/src/main/res/menu/menu_home_md2.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_log.xml b/app/src/main/res/menu/menu_log.xml deleted file mode 100644 index 7fc9e341b..000000000 --- a/app/src/main/res/menu/menu_log.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_log_md2.xml b/app/src/main/res/menu/menu_log_md2.xml new file mode 100644 index 000000000..0356092b2 --- /dev/null +++ b/app/src/main/res/menu/menu_log_md2.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_magiskhide.xml b/app/src/main/res/menu/menu_magiskhide.xml deleted file mode 100644 index 6a44ae6cf..000000000 --- a/app/src/main/res/menu/menu_magiskhide.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_module_md2.xml b/app/src/main/res/menu/menu_module_md2.xml new file mode 100644 index 000000000..71169adea --- /dev/null +++ b/app/src/main/res/menu/menu_module_md2.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_reboot.xml b/app/src/main/res/menu/menu_reboot.xml deleted file mode 100644 index fe09fd480..000000000 --- a/app/src/main/res/menu/menu_reboot.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_repo.xml b/app/src/main/res/menu/menu_repo.xml deleted file mode 100644 index 38f3e5b23..000000000 --- a/app/src/main/res/menu/menu_repo.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_superuser_md2.xml b/app/src/main/res/menu/menu_superuser_md2.xml new file mode 100644 index 000000000..c5e26ffd2 --- /dev/null +++ b/app/src/main/res/menu/menu_superuser_md2.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes_md2.xml b/app/src/main/res/values-night/themes_md2.xml new file mode 100644 index 000000000..d72ce1b6a --- /dev/null +++ b/app/src/main/res/values-night/themes_md2.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml deleted file mode 100644 index 90ae789ca..000000000 --- a/app/src/main/res/values-sw600dp/dimens.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - 24dp - \ No newline at end of file diff --git a/app/src/main/res/values-v19/styles_md2.xml b/app/src/main/res/values-v19/styles_md2.xml new file mode 100644 index 000000000..b3276387c --- /dev/null +++ b/app/src/main/res/values-v19/styles_md2.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v21/styles_md2_impl.xml b/app/src/main/res/values-v21/styles_md2_impl.xml new file mode 100644 index 000000000..ce1e57cba --- /dev/null +++ b/app/src/main/res/values-v21/styles_md2_impl.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 0beedc71e..aac4b4a38 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -91,4 +91,10 @@ @string/sort_by_update + + @string/settings_grid_span_count_1 + @string/settings_grid_span_count_2 + @string/settings_grid_span_count_3 + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index b6b8c507e..6c064a318 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -1,7 +1,22 @@ - + + - + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/default_color.xml b/app/src/main/res/values/default_color.xml new file mode 100644 index 000000000..93203ddd0 --- /dev/null +++ b/app/src/main/res/values/default_color.xml @@ -0,0 +1,24 @@ + + + + #4EAFF5 + #804EAFF5 + #3E78AF + #803E78AF + #F9F9F9 + #E8E8E8 + @color/defColorOnSurface + #F9F9F9 + #D9E6E6E6 + #F9F9F9 + @color/defColorOnSurface + #CC0047 + #F9F9F9 + #444444 + #80444444 + #808080 + #66808080 + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 6a96e19d0..1f6e3b328 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -3,13 +3,22 @@ 2dp 16dp - - 32dp 16dp 8dp - 8dp - + 2dp + 4dp + 8dp + 12dp + 16dp + 32dp + + 8dp + + + 80dp + + 56dp \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 77b8f273e..17fff2f0d 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -1,6 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d9a127161..87e0a26fd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -137,9 +137,11 @@ Update Channel Stable Beta - Custom + Custom Channel Insert a custom URL + Magisk Core Only Mode + Enable only core features. MagiskSU and MagiskHide will still be enabled, but no modules will be loaded Hide Magisk from various forms of detection Systemless hosts diff --git a/app/src/main/res/values/strings_md2.xml b/app/src/main/res/values/strings_md2.xml new file mode 100644 index 000000000..e013ca4f2 --- /dev/null +++ b/app/src/main/res/values/strings_md2.xml @@ -0,0 +1,109 @@ + + + + Manager + System + + Yes + No + + No connection available + + Home + @string/modules + @string/superuser + @string/log + @string/settings + Themes + + is not installed + has invalid update channel! + is up to date + can be updated! + is loading… + + \@topjohnwu + \@diareuse + Project links + PayPal + Patreon + Twitter + Source + XDA + + Always make sure you\'re using open-source Magisk Manager. Manager of unknown source can perform malicious actions. + Hide + Support Us + Magisk is, and always will be, free and open-source. You can however show us that you care by sending a small donation. + + Security + System + A/B + SAR + + Secure + Outdated + + Version + Code + Mode + Connection + Package + + Normal + Safe + Dynamic + + SafetyNet + + Hide + + Success! + Attestation failed! + Just a sec… + Try again + + Theme Mode + Select mode which best suits your style! + Always Light + Follow System + Always Dark + Safe Mode + Disables everything but essential functionality within Magisk and Magisk Manager. Magisk Hide, as a separate subsystem, will stand unaffected. + Grid Column Size + Sets column size for all eligible grid lists. You can set this outside settings by performing pinch gesture. + One item per line (Small Screens) + Two items per line (Recommended) + Three item per line (Tablet/TV) + + Options + Method + Next + Let\'s go + + You\'re in safe mode. None of user modules will work.\nThis message will disappear once safe mode is disabled. + %1$s by %2$s + Updates + Update all + Installed + Remote + Remove + Restore + Install from storage + Your modules are up to date! + No modules detected, try installing one from the list below. + + Logs + Notifications + Revoke + No app has asked for superuser permission yet. + + Filter by name + Scroll up + Filters + Search + + You\'re log-free, try using your SU enabled apps more. + Magisk logs are empty, that\'s weird. + + \ No newline at end of file diff --git a/app/src/main/res/values/styles_md2.xml b/app/src/main/res/values/styles_md2.xml new file mode 100644 index 000000000..a4b6094dd --- /dev/null +++ b/app/src/main/res/values/styles_md2.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles_md2_appearance.xml b/app/src/main/res/values/styles_md2_appearance.xml new file mode 100644 index 000000000..1a2ad65d6 --- /dev/null +++ b/app/src/main/res/values/styles_md2_appearance.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles_md2_impl.xml b/app/src/main/res/values/styles_md2_impl.xml new file mode 100644 index 000000000..627f21382 --- /dev/null +++ b/app/src/main/res/values/styles_md2_impl.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles_view_md2.xml b/app/src/main/res/values/styles_view_md2.xml new file mode 100644 index 000000000..2355e8f5e --- /dev/null +++ b/app/src/main/res/values/styles_view_md2.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes_md2.xml b/app/src/main/res/values/themes_md2.xml new file mode 100644 index 000000000..6fcbf15fb --- /dev/null +++ b/app/src/main/res/values/themes_md2.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/app_settings.xml b/app/src/main/res/xml/app_settings.xml index 6fe5e52c6..c4149e9d5 100644 --- a/app/src/main/res/xml/app_settings.xml +++ b/app/src/main/res/xml/app_settings.xml @@ -1,4 +1,5 @@ - + @@ -131,4 +132,15 @@ + + + + + + diff --git a/build.gradle b/build.gradle index 7e16c37fa..fdf406baa 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { maven { url 'https://kotlin.bintray.com/kotlinx' } } dependencies { - classpath 'com.android.tools.build:gradle:3.5.3' + classpath 'com.android.tools.build:gradle:3.6.0-rc01' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${vKotlin}" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ca9d62814..e867a4ae8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sun Jan 12 04:46:30 CST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip