diff --git a/README.MD b/README.MD index 83d542432..2a5be0cb5 100644 --- a/README.MD +++ b/README.MD @@ -33,7 +33,6 @@ Furthermore, Magisk provides a **Systemless Interface** to alter the system (or Default string resources for Magisk Manager are scattered throughout - `app/src/main/res/values/strings.xml` -- `stub/src/main/res/values/strings.xml` - `shared/src/main/res/values/strings.xml` Translate each and place them in the respective locations (`/src/main/res/values-/strings.xml`). diff --git a/app/build.gradle b/app/build.gradle index f8881e830..7e992561e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,14 +60,26 @@ dependencies { implementation 'com.github.topjohnwu:jtar:1.0.0' implementation 'com.jakewharton.timber:timber:4.7.1' - implementation 'com.github.skoumalcz:teanity:0.3.3' implementation 'com.ncapdevi:frag-nav:3.2.0' implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6' - def vMarkwon = '3.1.0' - implementation "ru.noties.markwon:core:${vMarkwon}" - implementation "ru.noties.markwon:html:${vMarkwon}" - implementation "ru.noties.markwon:image-svg:${vMarkwon}" + implementation 'io.reactivex.rxjava2:rxjava:2.2.13' + implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + + implementation "org.jetbrains.kotlin:kotlin-stdlib:${vKotlin}" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${vKotlin}" + + def vBAdapt = '3.1.1' + def bindingAdapter = 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter' + implementation "${bindingAdapter}:${vBAdapt}" + implementation "${bindingAdapter}-recyclerview:${vBAdapt}" + + def vMarkwon = '4.1.1' + implementation "io.noties.markwon:core:${vMarkwon}" + implementation "io.noties.markwon:html:${vMarkwon}" + implementation "io.noties.markwon:image:${vMarkwon}" + implementation 'com.caverock:androidsvg:1.4' def vLibsu = '2.5.1' implementation "com.github.topjohnwu.libsu:core:${vLibsu}" @@ -78,13 +90,13 @@ dependencies { implementation "org.koin:koin-android:${vKoin}" implementation "org.koin:koin-androidx-viewmodel:${vKoin}" - def vRetrofit = '2.6.1' + def vRetrofit = '2.6.2' 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.2' + def vOkHttp = '3.12.6' implementation "com.squareup.okhttp3:okhttp:${vOkHttp}" implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}" @@ -100,19 +112,21 @@ dependencies { replacedBy('com.github.topjohnwu:room-runtime') } } - def vRoom = "2.1.0" + def vRoom = "2.2.0" implementation "com.github.topjohnwu:room-runtime:${vRoom}" kapt "androidx.room:room-compiler:${vRoom}" - implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVer}" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVer}" + def vNav = "2.1.0" + implementation "androidx.navigation:navigation-fragment-ktx:$vNav" + implementation "androidx.navigation:navigation-ui-ktx:$vNav" implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'androidx.browser:browser:1.0.0' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03' implementation 'androidx.preference:preference:1.1.0' - implementation 'androidx.recyclerview:recyclerview:1.1.0-beta04' + implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'androidx.work:work-runtime:2.2.0' - implementation 'androidx.transition:transition:1.2.0-rc01' + implementation 'androidx.transition:transition:1.2.0' implementation 'androidx.multidex:multidex:2.0.1' + implementation 'androidx.core:core-ktx:1.1.0' implementation 'com.google.android.material:material:1.1.0-beta01' } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 37a7fea13..c95d10abb 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -29,7 +29,7 @@ } # DelegateWorker --keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker +-keep,allowobfuscation class * extends com.topjohnwu.magisk.base.DelegateWorker # BootSigner -keepclassmembers class com.topjohnwu.signing.BootSigner { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aa809b3b2..8d47f905a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,4 +1,22 @@ + + + @@ -11,25 +29,17 @@ + tools:ignore="UnusedAttribute,GoogleAppIndexingWarning" + tools:replace="android:appComponentFactory"> - + - - @@ -37,6 +47,18 @@ + + + + + + @@ -46,11 +68,12 @@ + + + android:screenOrientation="nosensor" /> @@ -58,8 +81,7 @@ android:name="a.m" android:directBootAware="true" android:excludeFromRecents="true" - android:exported="false" - android:theme="@style/MagiskTheme.SU" /> + android:exported="false" /> @@ -78,9 +100,10 @@ - + - diff --git a/app/src/main/java/a/a.java b/app/src/main/java/a/a.java index ec5a4e698..5ccb6738b 100644 --- a/app/src/main/java/a/a.java +++ b/app/src/main/java/a/a.java @@ -1,13 +1,19 @@ package a; +import androidx.annotation.Keep; +import androidx.core.app.AppComponentFactory; + import com.topjohnwu.magisk.utils.PatchAPK; import com.topjohnwu.signing.BootSigner; -import androidx.annotation.Keep; - @Keep -public class a extends BootSigner { +public class a extends AppComponentFactory { + public static boolean patchAPK(String in, String out, String pkg) { return PatchAPK.patch(in, out, pkg); } + + public static void main(String[] args) throws Exception { + BootSigner.main(args); + } } diff --git a/app/src/main/java/a/w.java b/app/src/main/java/a/w.java index 17572db75..6fa3e50f6 100644 --- a/app/src/main/java/a/w.java +++ b/app/src/main/java/a/w.java @@ -6,7 +6,8 @@ import androidx.annotation.NonNull; import androidx.work.Worker; import androidx.work.WorkerParameters; -import com.topjohnwu.magisk.model.worker.DelegateWorker; +import com.topjohnwu.magisk.base.DelegateWorker; +import com.topjohnwu.magisk.utils.ResourceMgrKt; import java.lang.reflect.ParameterizedType; @@ -18,7 +19,7 @@ public abstract class w extends Worker { @SuppressWarnings("unchecked") w(@NonNull Context context, @NonNull WorkerParameters workerParams) { - super(context, workerParams); + super(ResourceMgrKt.wrap(context, false), workerParams); try { base = ((Class) ((ParameterizedType) getClass().getGenericSuperclass()) .getActualTypeArguments()[0]).newInstance(); diff --git a/app/src/main/java/com/topjohnwu/magisk/App.kt b/app/src/main/java/com/topjohnwu/magisk/App.kt index 1224d400f..3a3f6961c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/App.kt +++ b/app/src/main/java/com/topjohnwu/magisk/App.kt @@ -13,9 +13,11 @@ import com.topjohnwu.magisk.data.database.RepoDatabase_Impl import com.topjohnwu.magisk.di.ActivityTracker import com.topjohnwu.magisk.di.koinModules import com.topjohnwu.magisk.extensions.get -import com.topjohnwu.magisk.net.Networking -import com.topjohnwu.magisk.utils.LocaleManager -import com.topjohnwu.magisk.utils.RootUtils +import com.topjohnwu.magisk.extensions.unwrap +import com.topjohnwu.magisk.utils.ResourceMgr +import com.topjohnwu.magisk.utils.RootInit +import com.topjohnwu.magisk.utils.isRunningAsStub +import com.topjohnwu.magisk.utils.wrap import com.topjohnwu.superuser.Shell import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin @@ -27,7 +29,7 @@ open class App : Application() { AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX) Shell.Config.verboseLogging(BuildConfig.DEBUG) - Shell.Config.addInitializers(RootUtils::class.java) + Shell.Config.addInitializers(RootInit::class.java) Shell.Config.setTimeout(2) Room.setFactory { when (it) { @@ -39,24 +41,42 @@ open class App : Application() { } override fun attachBaseContext(base: Context) { - super.attachBaseContext(base) + // Basic setup if (BuildConfig.DEBUG) MultiDex.install(base) Timber.plant(Timber.DebugTree()) + // Some context magic + val app: Application + val impl: Context + if (base is Application) { + isRunningAsStub = true + app = base + impl = base.baseContext + } else { + app = this + impl = base + } + ResourceMgr.init(impl) + super.attachBaseContext(impl.wrap()) + + // Normal startup startKoin { - androidContext(this@App) + androidContext(baseContext) modules(koinModules) } + ResourceMgr.reload() + app.registerActivityLifecycleCallbacks(get()) + } - registerActivityLifecycleCallbacks(get()) - - Networking.init(base) - LocaleManager.setLocale(this) + // This is required as some platforms expect ContextImpl + override fun getBaseContext(): Context { + return super.getBaseContext().unwrap() } override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - LocaleManager.setLocale(this) + ResourceMgr.reload(newConfig) + if (!isRunningAsStub) + super.onConfigurationChanged(newConfig) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/Const.kt b/app/src/main/java/com/topjohnwu/magisk/Const.kt index aa963451e..2f29cfb96 100644 --- a/app/src/main/java/com/topjohnwu/magisk/Const.kt +++ b/app/src/main/java/com/topjohnwu/magisk/Const.kt @@ -13,8 +13,8 @@ object Const { // Versions const val SNET_EXT_VER = 13 - const val SNET_REVISION = "5adbc435ce93ded953c30ebe587edfd50b5503bc" - const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77" + const val SNET_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df" + const val BOOTCTL_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df" // Misc const val ANDROID_MANIFEST = "AndroidManifest.xml" diff --git a/app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt b/app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt new file mode 100644 index 000000000..0b8fed26f --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt @@ -0,0 +1,124 @@ +package com.topjohnwu.magisk.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.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.utils.wrap +import kotlin.random.Random + +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 + + 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) + super.applyOverrideConfiguration(config) + } + + override fun attachBaseContext(base: Context) { + super.attachBaseContext(base.wrap(false)) + } + + 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@BaseActivity + } + } + + fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) { + val request = PermissionRequestBuilder().apply(builder).build() + val ungranted = permissions.filter { + ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED + } + + if (ungranted.isEmpty()) { + request.onSuccess() + } else { + val requestCode = Random.nextInt(256, 512) + resultCallbacks[requestCode] = { result, _ -> + if (result > 0) + request.onSuccess() + else + request.onFailure() + } + ActivityCompat.requestPermissions(this, ungranted.toTypedArray(), requestCode) + } + } + + fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) { + withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder) + } + + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray) { + var success = true + for (res in grantResults) { + if (res != PackageManager.PERMISSION_GRANTED) { + success = false + break + } + } + resultCallbacks[requestCode]?.apply { + resultCallbacks.remove(requestCode) + invoke(this@BaseActivity, if (success) 1 else -1, null) + } + + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + resultCallbacks[requestCode]?.apply { + resultCallbacks.remove(requestCode) + invoke(this@BaseActivity, resultCode, data) + } + } + + fun startActivityForResult(intent: Intent, requestCode: Int, listener: RequestCallback) { + resultCallbacks[requestCode] = listener + startActivityForResult(intent, requestCode) + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt b/app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt new file mode 100644 index 000000000..368bb4e91 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt @@ -0,0 +1,50 @@ +package com.topjohnwu.magisk.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 : + Fragment(), EventHandler { + + protected val activity get() = requireActivity() as BaseActivity<*, *> + protected lateinit var binding: Binding + protected abstract val layoutRes: Int + protected abstract val viewModel: ViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel.viewEvents.observe(this, viewEventObserver) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = DataBindingUtil.inflate(inflater, layoutRes, container, false).apply { + setVariable(BR.viewModel, viewModel) + lifecycleOwner = this@BaseFragment + } + + return binding.root + } + + @CallSuper + override fun onEventDispatched(event: ViewEvent) { + super.onEventDispatched(event) + activity.onEventDispatched(event) + } + + open fun onBackPressed(): Boolean = false + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/base/BasePreferenceFragment.kt b/app/src/main/java/com/topjohnwu/magisk/base/BasePreferenceFragment.kt new file mode 100644 index 000000000..01aa437c0 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/base/BasePreferenceFragment.kt @@ -0,0 +1,56 @@ +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/BaseReceiver.kt b/app/src/main/java/com/topjohnwu/magisk/base/BaseReceiver.kt new file mode 100644 index 000000000..2e96a6b11 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/base/BaseReceiver.kt @@ -0,0 +1,17 @@ +package com.topjohnwu.magisk.base + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import com.topjohnwu.magisk.utils.wrap +import org.koin.core.KoinComponent + +abstract class BaseReceiver : BroadcastReceiver(), KoinComponent { + + final override fun onReceive(context: Context, intent: Intent?) { + onReceive(context.wrap() as ContextWrapper, intent) + } + + abstract fun onReceive(context: ContextWrapper, intent: Intent?) +} diff --git a/app/src/main/java/com/topjohnwu/magisk/base/BaseService.kt b/app/src/main/java/com/topjohnwu/magisk/base/BaseService.kt new file mode 100644 index 000000000..dc7801664 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/base/BaseService.kt @@ -0,0 +1,12 @@ +package com.topjohnwu.magisk.base + +import android.app.Service +import android.content.Context +import com.topjohnwu.magisk.utils.wrap +import org.koin.core.KoinComponent + +abstract class BaseService : Service(), KoinComponent { + override fun attachBaseContext(base: Context) { + super.attachBaseContext(base.wrap()) + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/worker/DelegateWorker.kt b/app/src/main/java/com/topjohnwu/magisk/base/DelegateWorker.kt similarity index 96% rename from app/src/main/java/com/topjohnwu/magisk/model/worker/DelegateWorker.kt rename to app/src/main/java/com/topjohnwu/magisk/base/DelegateWorker.kt index fe91cdf82..fd5e6e8f2 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/worker/DelegateWorker.kt +++ b/app/src/main/java/com/topjohnwu/magisk/base/DelegateWorker.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.model.worker +package com.topjohnwu.magisk.base import android.content.Context import android.net.Network diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/BaseViewModel.kt similarity index 75% rename from app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt rename to app/src/main/java/com/topjohnwu/magisk/base/viewmodel/BaseViewModel.kt index c1906cbd9..a9acc6ebb 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/BaseViewModel.kt @@ -1,24 +1,23 @@ -package com.topjohnwu.magisk.ui.base +package com.topjohnwu.magisk.base.viewmodel import android.app.Activity import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork -import com.skoumal.teanity.extensions.doOnSubscribeUi -import com.skoumal.teanity.extensions.subscribeK -import com.skoumal.teanity.util.KObservableField -import com.skoumal.teanity.viewmodel.LoadingViewModel +import com.topjohnwu.magisk.extensions.doOnSubscribeUi import com.topjohnwu.magisk.extensions.get +import com.topjohnwu.magisk.extensions.subscribeK 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 -abstract class MagiskViewModel( +abstract class BaseViewModel( initialState: State = State.LOADING ) : LoadingViewModel(initialState) { - val isConnected = KObservableField(true) + val isConnected = KObservableField(false) init { ReactiveNetwork.observeNetworkConnectivity(get()) 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 new file mode 100644 index 000000000..bd4ac195d --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/LoadingViewModel.kt @@ -0,0 +1,78 @@ +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 new file mode 100644 index 000000000..17ea6f373 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/ObservableViewModel.kt @@ -0,0 +1,46 @@ +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 new file mode 100644 index 000000000..e441c84db --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/StatefulViewModel.kt @@ -0,0 +1,15 @@ +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 new file mode 100644 index 000000000..4fb10599a --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/base/viewmodel/TeanityViewModel.kt @@ -0,0 +1,33 @@ +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/data/network/GithubServices.kt b/app/src/main/java/com/topjohnwu/magisk/data/network/GithubServices.kt index 04cbadcfe..1a10b592f 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 @@ -19,10 +19,10 @@ interface GithubRawServices { @GET("$MAGISK_FILES/master/beta.json") fun fetchBetaUpdate(): Single - @GET("$MAGISK_FILES/master/canary_builds/release.json") + @GET("$MAGISK_FILES/canary/release.json") fun fetchCanaryUpdate(): Single - @GET("$MAGISK_FILES/master/canary_builds/canary.json") + @GET("$MAGISK_FILES/canary/debug.json") fun fetchCanaryDebugUpdate(): Single @GET diff --git a/app/src/main/java/com/topjohnwu/magisk/databinding/AdaptersGeneric.kt b/app/src/main/java/com/topjohnwu/magisk/databinding/AdaptersGeneric.kt new file mode 100644 index 000000000..0038cb7af --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/databinding/AdaptersGeneric.kt @@ -0,0 +1,26 @@ +package com.topjohnwu.magisk.databinding + +import android.view.View +import androidx.core.view.isGone +import androidx.core.view.isInvisible +import androidx.databinding.BindingAdapter + +@BindingAdapter("gone") +fun setGone(view: View, gone: Boolean) { + view.isGone = gone +} + +@BindingAdapter("invisible") +fun setInvisible(view: View, invisible: Boolean) { + view.isInvisible = invisible +} + +@BindingAdapter("goneUnless") +fun setGoneUnless(view: View, goneUnless: Boolean) { + setGone(view, goneUnless.not()) +} + +@BindingAdapter("invisibleUnless") +fun setInvisibleUnless(view: View, invisibleUnless: Boolean) { + setInvisible(view, invisibleUnless.not()) +} diff --git a/app/src/main/java/com/topjohnwu/magisk/databinding/AdaptersRecycler.kt b/app/src/main/java/com/topjohnwu/magisk/databinding/AdaptersRecycler.kt new file mode 100644 index 000000000..9815383b3 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/databinding/AdaptersRecycler.kt @@ -0,0 +1,57 @@ +package com.topjohnwu.magisk.databinding + +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.InsetDrawable +import androidx.databinding.BindingAdapter +import androidx.recyclerview.widget.RecyclerView +import com.topjohnwu.magisk.extensions.startEndToLeftRight +import com.topjohnwu.magisk.extensions.toPx +import com.topjohnwu.magisk.utils.KItemDecoration +import kotlin.math.roundToInt + +@BindingAdapter( + "dividerColor", + "dividerHorizontal", + "dividerSize", + "dividerAfterLast", + "dividerMarginStart", + "dividerMarginEnd", + "dividerMarginTop", + "dividerMarginBottom", + requireAll = false +) +fun setDivider( + view: RecyclerView, + color: Int, + horizontal: Boolean, + _size: Float, + _afterLast: Boolean?, + marginStartF: Float, + marginEndF: Float, + marginTopF: Float, + marginBottomF: Float +) { + val orientation = if (horizontal) RecyclerView.HORIZONTAL else RecyclerView.VERTICAL + val size = if (_size > 0) _size.roundToInt() else 1.toPx() + val (width, height) = if (horizontal) size to 1 else 1 to size + val afterLast = _afterLast ?: true + + val marginStart = marginStartF.roundToInt() + val marginEnd = marginEndF.roundToInt() + val marginTop = marginTopF.roundToInt() + val marginBottom = marginBottomF.roundToInt() + val (marginLeft, marginRight) = view.context.startEndToLeftRight(marginStart, marginEnd) + + val drawable = GradientDrawable().apply { + setSize(width, height) + shape = GradientDrawable.RECTANGLE + setColor(color) + }.let { + InsetDrawable(it, marginLeft, marginTop, marginRight, marginBottom) + } + + val decoration = KItemDecoration(view.context, orientation) + .setDeco(drawable) + .apply { showAfterLast = afterLast } + view.addItemDecoration(decoration) +} diff --git a/app/src/main/java/com/topjohnwu/magisk/databinding/BindingBoundAdapter.kt b/app/src/main/java/com/topjohnwu/magisk/databinding/BindingBoundAdapter.kt new file mode 100644 index 000000000..39d439303 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/databinding/BindingBoundAdapter.kt @@ -0,0 +1,13 @@ +package com.topjohnwu.magisk.databinding + +import androidx.databinding.ViewDataBinding +import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter + +open class BindingBoundAdapter : BindingRecyclerViewAdapter() { + + override fun onBindBinding(binding: ViewDataBinding, variableId: Int, layoutRes: Int, position: Int, item: RvItem) { + super.onBindBinding(binding, variableId, layoutRes, position, item) + + item.onBindingBound(binding) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/databinding/RecyclerViewItems.kt b/app/src/main/java/com/topjohnwu/magisk/databinding/RecyclerViewItems.kt new file mode 100644 index 000000000..087a753f1 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/databinding/RecyclerViewItems.kt @@ -0,0 +1,48 @@ +package com.topjohnwu.magisk.databinding + +import androidx.annotation.CallSuper +import androidx.databinding.ViewDataBinding +import com.topjohnwu.magisk.BR +import com.topjohnwu.magisk.utils.DiffObservableList +import me.tatarka.bindingcollectionadapter2.ItemBinding + +abstract class RvItem { + + abstract val layoutRes: Int + + @CallSuper + open fun bind(binding: ItemBinding<*>) { + binding.set(BR.item, layoutRes) + } + + /** + * This callback is useful if you want to manipulate your views directly. + * If you want to use this callback, you must set [me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter] + * on your RecyclerView and call it from there. You can use [BindingBoundAdapter] for your convenience. + */ + open fun onBindingBound(binding: ViewDataBinding) {} +} + +abstract class ComparableRvItem : RvItem() { + + abstract fun itemSameAs(other: T): Boolean + abstract fun contentSameAs(other: T): Boolean + @Suppress("UNCHECKED_CAST") + open fun genericItemSameAs(other: Any): Boolean = other::class == this::class && itemSameAs(other as T) + @Suppress("UNCHECKED_CAST") + open fun genericContentSameAs(other: Any): Boolean = other::class == this::class && contentSameAs(other as T) + + companion object { + val callback = object : DiffObservableList.Callback> { + override fun areItemsTheSame( + oldItem: ComparableRvItem<*>, + newItem: ComparableRvItem<*> + ) = oldItem.genericItemSameAs(newItem) + + override fun areContentsTheSame( + oldItem: ComparableRvItem<*>, + newItem: ComparableRvItem<*> + ) = oldItem.genericContentSameAs(newItem) + } + } +} \ 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 d03cec643..437c69c3a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt @@ -7,7 +7,7 @@ import android.content.Context import android.os.Build import android.os.Bundle import androidx.preference.PreferenceManager -import com.skoumal.teanity.rxbus.RxBus +import com.topjohnwu.magisk.utils.RxBus import org.koin.core.qualifier.named import org.koin.dsl.module 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 3824a0c97..73db91cb9 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt @@ -1,11 +1,18 @@ package com.topjohnwu.magisk.di +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.data.network.GithubApiServices import com.topjohnwu.magisk.data.network.GithubRawServices +import com.topjohnwu.magisk.net.Networking +import com.topjohnwu.magisk.net.NoSSLv3SocketFactory +import io.noties.markwon.Markwon +import io.noties.markwon.html.HtmlPlugin +import io.noties.markwon.image.ImagesPlugin +import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.koin.dsl.module @@ -16,14 +23,15 @@ import retrofit2.converter.scalars.ScalarsConverterFactory import se.ansman.kotshi.KotshiJsonAdapterFactory val networkingModule = module { - single { createOkHttpClient() } - single { createMoshiConverterFactory() } - single { createRetrofit(get(), get()) } + single { createOkHttpClient(get()) } + single { createRetrofit(get()) } single { createApiService(get(), Const.Url.GITHUB_RAW_URL) } single { createApiService(get(), Const.Url.GITHUB_API_URL) } + single { createMarkwon(get(), get()) } } -fun createOkHttpClient(): OkHttpClient { +@Suppress("DEPRECATION") +fun createOkHttpClient(context: Context): OkHttpClient { val builder = OkHttpClient.Builder() if (BuildConfig.DEBUG) { @@ -33,6 +41,10 @@ fun createOkHttpClient(): OkHttpClient { builder.addInterceptor(httpLoggingInterceptor) } + if (!Networking.init(context)) { + builder.sslSocketFactory(NoSSLv3SocketFactory()) + } + return builder.build() } @@ -43,13 +55,10 @@ fun createMoshiConverterFactory(): MoshiConverterFactory { return MoshiConverterFactory.create(moshi) } -fun createRetrofit( - okHttpClient: OkHttpClient, - converterFactory: MoshiConverterFactory -): Retrofit.Builder { +fun createRetrofit(okHttpClient: OkHttpClient): Retrofit.Builder { return Retrofit.Builder() .addConverterFactory(ScalarsConverterFactory.create()) - .addConverterFactory(converterFactory) + .addConverterFactory(createMoshiConverterFactory()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .client(okHttpClient) } @@ -62,4 +71,13 @@ inline fun createApiService(retrofitBuilder: Retrofit.Builder, baseU .baseUrl(baseUrl) .build() .create(T::class.java) -} \ No newline at end of file +} + +fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon { + return Markwon.builder(context) + .usePlugin(HtmlPlugin.create()) + .usePlugin(ImagesPlugin.create { + it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient)) + }) + .build() +} diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/DataBinding.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/DataBinding.kt new file mode 100644 index 000000000..f671815a1 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/DataBinding.kt @@ -0,0 +1,57 @@ +package com.topjohnwu.magisk.extensions + +import androidx.databinding.Observable +import androidx.databinding.ObservableBoolean +import androidx.databinding.ObservableField +import androidx.databinding.ObservableInt + +fun ObservableField.addOnPropertyChangedCallback( + removeAfterChanged: Boolean = false, + callback: (T?) -> Unit +) { + addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable?, propertyId: Int) { + callback(get()) + if (removeAfterChanged) removeOnPropertyChangedCallback(this) + } + }) +} + +fun ObservableInt.addOnPropertyChangedCallback( + removeAfterChanged: Boolean = false, + callback: (Int) -> Unit +) { + addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable?, propertyId: Int) { + callback(get()) + if (removeAfterChanged) removeOnPropertyChangedCallback(this) + } + }) +} + +fun ObservableBoolean.addOnPropertyChangedCallback( + removeAfterChanged: Boolean = false, + callback: (Boolean) -> Unit +) { + addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable?, propertyId: Int) { + callback(get()) + if (removeAfterChanged) removeOnPropertyChangedCallback(this) + } + }) +} + +inline fun ObservableField.update(block: (T?) -> Unit) { + set(get().apply(block)) +} + +inline fun ObservableField.updateNonNull(block: (T) -> Unit) { + update { + it ?: return@update + block(it) + } +} + +inline fun ObservableInt.update(block: (Int) -> Unit) { + set(get().apply(block)) +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/Dimens.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/Dimens.kt new file mode 100644 index 000000000..77c5d3439 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/Dimens.kt @@ -0,0 +1,9 @@ +package com.topjohnwu.magisk.extensions + +import android.content.res.Resources +import kotlin.math.ceil +import kotlin.math.roundToInt + +fun Int.toDp(): Int = ceil(this / Resources.getSystem().displayMetrics.density).roundToInt() + +fun Int.toPx(): Int = (this * Resources.getSystem().displayMetrics.density).roundToInt() diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/Misc.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/Misc.kt new file mode 100644 index 000000000..b3a54ab4f --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/Misc.kt @@ -0,0 +1,6 @@ +package com.topjohnwu.magisk.extensions + +import android.os.Handler +import android.os.Looper + +fun ui(body: () -> Unit) = Handler(Looper.getMainLooper()).post(body) \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt new file mode 100644 index 000000000..a4eefb0e9 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt @@ -0,0 +1,201 @@ +package com.topjohnwu.magisk.extensions + +import androidx.databinding.ObservableField +import com.topjohnwu.magisk.utils.KObservableField +import io.reactivex.* +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposables +import io.reactivex.functions.BiFunction +import io.reactivex.schedulers.Schedulers +import androidx.databinding.Observable as BindingObservable + +fun Observable.applySchedulers( + subscribeOn: Scheduler = Schedulers.io(), + observeOn: Scheduler = AndroidSchedulers.mainThread() +): Observable = this.subscribeOn(subscribeOn).observeOn(observeOn) + +fun Flowable.applySchedulers( + subscribeOn: Scheduler = Schedulers.io(), + observeOn: Scheduler = AndroidSchedulers.mainThread() +): Flowable = this.subscribeOn(subscribeOn).observeOn(observeOn) + +fun Single.applySchedulers( + subscribeOn: Scheduler = Schedulers.io(), + observeOn: Scheduler = AndroidSchedulers.mainThread() +): Single = this.subscribeOn(subscribeOn).observeOn(observeOn) + +fun Maybe.applySchedulers( + subscribeOn: Scheduler = Schedulers.io(), + observeOn: Scheduler = AndroidSchedulers.mainThread() +): Maybe = this.subscribeOn(subscribeOn).observeOn(observeOn) + +fun Completable.applySchedulers( + subscribeOn: Scheduler = Schedulers.io(), + observeOn: Scheduler = AndroidSchedulers.mainThread() +): Completable = this.subscribeOn(subscribeOn).observeOn(observeOn) + +/*=== ALIASES FOR OBSERVABLES ===*/ + +typealias OnCompleteListener = () -> Unit +typealias OnSuccessListener = (T) -> Unit +typealias OnErrorListener = (Throwable) -> Unit + +/*=== ALIASES FOR OBSERVABLES ===*/ + +fun Observable.subscribeK( + onError: OnErrorListener = { it.printStackTrace() }, + onComplete: OnCompleteListener = {}, + onNext: OnSuccessListener = {} +) = applySchedulers() + .subscribe(onNext, onError, onComplete) + +fun Single.subscribeK( + onError: OnErrorListener = { it.printStackTrace() }, + onNext: OnSuccessListener = {} +) = applySchedulers() + .subscribe(onNext, onError) + +fun Maybe.subscribeK( + onError: OnErrorListener = { it.printStackTrace() }, + onComplete: OnCompleteListener = {}, + onNext: OnSuccessListener = {} +) = applySchedulers() + .subscribe(onNext, onError, onComplete) + +fun Flowable.subscribeK( + onError: OnErrorListener = { it.printStackTrace() }, + onComplete: OnCompleteListener = {}, + onNext: OnSuccessListener = {} +) = applySchedulers() + .subscribe(onNext, onError, onComplete) + +fun Completable.subscribeK( + onError: OnErrorListener = { it.printStackTrace() }, + onComplete: OnCompleteListener = {} +) = applySchedulers() + .subscribe(onComplete, onError) + + +fun Observable.updateBy( + field: KObservableField +) = doOnNextUi { field.value = it } + .doOnErrorUi { field.value = null } + +fun Single.updateBy( + field: KObservableField +) = doOnSuccessUi { field.value = it } + .doOnErrorUi { field.value = null } + +fun Maybe.updateBy( + field: KObservableField +) = doOnSuccessUi { field.value = it } + .doOnErrorUi { field.value = null } + .doOnComplete { field.value = field.value } + +fun Flowable.updateBy( + field: KObservableField +) = doOnNextUi { field.value = it } + .doOnErrorUi { field.value = null } + +fun Completable.updateBy( + field: KObservableField +) = doOnCompleteUi { field.value = true } + .doOnErrorUi { field.value = false } + + +fun Observable.doOnSubscribeUi(body: () -> Unit) = + doOnSubscribe { ui { body() } } + +fun Single.doOnSubscribeUi(body: () -> Unit) = + doOnSubscribe { ui { body() } } + +fun Maybe.doOnSubscribeUi(body: () -> Unit) = + doOnSubscribe { ui { body() } } + +fun Flowable.doOnSubscribeUi(body: () -> Unit) = + doOnSubscribe { ui { body() } } + +fun Completable.doOnSubscribeUi(body: () -> Unit) = + doOnSubscribe { ui { body() } } + + +fun Observable.doOnErrorUi(body: (Throwable) -> Unit) = + doOnError { ui { body(it) } } + +fun Single.doOnErrorUi(body: (Throwable) -> Unit) = + doOnError { ui { body(it) } } + +fun Maybe.doOnErrorUi(body: (Throwable) -> Unit) = + doOnError { ui { body(it) } } + +fun Flowable.doOnErrorUi(body: (Throwable) -> Unit) = + doOnError { ui { body(it) } } + +fun Completable.doOnErrorUi(body: (Throwable) -> Unit) = + doOnError { ui { body(it) } } + + +fun Observable.doOnNextUi(body: (T) -> Unit) = + doOnNext { ui { body(it) } } + +fun Flowable.doOnNextUi(body: (T) -> Unit) = + doOnNext { ui { body(it) } } + +fun Single.doOnSuccessUi(body: (T) -> Unit) = + doOnSuccess { ui { body(it) } } + +fun Maybe.doOnSuccessUi(body: (T) -> Unit) = + doOnSuccess { ui { body(it) } } + +fun Maybe.doOnCompleteUi(body: () -> Unit) = + doOnComplete { ui { body() } } + +fun Completable.doOnCompleteUi(body: () -> Unit) = + doOnComplete { ui { body() } } + + +fun Observable>.mapList( + transformer: (T) -> R +) = flatMapIterable { it } + .map(transformer) + .toList() + +fun Single>.mapList( + transformer: (T) -> R +) = flattenAsFlowable { it } + .map(transformer) + .toList() + +fun Maybe>.mapList( + transformer: (T) -> R +) = flattenAsFlowable { it } + .map(transformer) + .toList() + +fun Flowable>.mapList( + transformer: (T) -> R +) = flatMapIterable { it } + .map(transformer) + .toList() + +fun ObservableField.toObservable(): Observable { + val observableField = this + return Observable.create { emitter -> + observableField.get()?.let { emitter.onNext(it) } + + val callback = object : BindingObservable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: BindingObservable?, propertyId: Int) { + observableField.get()?.let { emitter.onNext(it) } + } + } + observableField.addOnPropertyChangedCallback(callback) + emitter.setDisposable(Disposables.fromAction { + observableField.removeOnPropertyChangedCallback(callback) + }) + } +} + +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) }) diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/Snackbar.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/Snackbar.kt new file mode 100644 index 000000000..a9e5e133f --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/Snackbar.kt @@ -0,0 +1,126 @@ +package com.topjohnwu.magisk.extensions + +import android.content.Context +import android.content.res.ColorStateList +import android.view.View +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.fragment.app.Fragment +import com.google.android.material.snackbar.Snackbar + +fun AppCompatActivity.snackbar( + view: View, + @StringRes messageRes: Int, + length: Int = Snackbar.LENGTH_SHORT, + f: Snackbar.() -> Unit = {} +) { + snackbar(view, getString(messageRes), length, f) +} + +fun AppCompatActivity.snackbar( + view: View, + message: String, + length: Int = Snackbar.LENGTH_SHORT, + f: Snackbar.() -> Unit = {} +) = Snackbar.make(view, message, length) + .apply(f) + .show() + +fun Fragment.snackbar( + view: View, + @StringRes messageRes: Int, + length: Int = Snackbar.LENGTH_SHORT, + f: Snackbar.() -> Unit = {} +) { + snackbar(view, getString(messageRes), length, f) +} + +fun Fragment.snackbar( + view: View, + message: String, + length: Int = Snackbar.LENGTH_SHORT, + f: Snackbar.() -> Unit = {} +) = Snackbar.make(view, message, length) + .apply(f) + .show() + +fun Snackbar.action(init: KSnackbar.() -> Unit) = apply { + val config = KSnackbar().apply(init) + + setAction(config.title(context), config.onClickListener) + + when { + config.hasValidColor -> setActionTextColor(config.color(context) ?: return@apply) + config.hasValidColorStateList -> setActionTextColor(config.colorStateList(context) ?: return@apply) + } +} + +class KSnackbar { + var colorRes: Int = -1 + var colorStateListRes: Int = -1 + + var title: CharSequence = "" + var titleRes: Int = -1 + + internal var onClickListener: (View) -> Unit = {} + internal val hasValidColor get() = colorRes != -1 + internal val hasValidColorStateList get() = colorStateListRes != -1 + + fun onClicked(listener: (View) -> Unit) { + onClickListener = listener + } + + internal fun title(context: Context) = if (title.isBlank()) context.getString(titleRes) else title + internal fun colorStateList(context: Context) = context.colorStateListCompat(colorStateListRes) + internal fun color(context: Context) = context.colorCompat(colorRes) +} + +@Deprecated("Kotlin DSL version is preferred", ReplaceWith("action {}")) +fun Snackbar.action( + @StringRes actionRes: Int, + @ColorRes colorRes: Int? = null, + listener: (View) -> Unit +) { + view.resources.getString(actionRes) + colorRes?.let { ContextCompat.getColor(view.context, colorRes) } + action {} +} + +@Deprecated("Kotlin DSL version is preferred", ReplaceWith("action {}")) +fun Snackbar.action(action: String, @ColorInt color: Int? = null, listener: (View) -> Unit) { + setAction(action, listener) + color?.let { setActionTextColor(color) } +} + +fun Snackbar.textColorRes(@ColorRes colorRes: Int) { + textColor(context.colorCompat(colorRes) ?: return) +} + +fun Snackbar.textColor(@ColorInt color: Int) { + val tv = view.findViewById(com.google.android.material.R.id.snackbar_text) + tv.setTextColor(color) +} + +fun Snackbar.backgroundColorRes(@ColorRes colorRes: Int) { + backgroundColor(context.colorCompat(colorRes) ?: return) +} + +fun Snackbar.backgroundColor(@ColorInt color: Int) { + ViewCompat.setBackgroundTintList( + view, + ColorStateList.valueOf(color) + ) +} + +fun Snackbar.alert() { + textColor(0xF44336) +} + +fun Snackbar.success() { + textColor(0x4CAF50) +} \ 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 abf5e81c9..4a893ab8b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/XAndroid.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/XAndroid.kt @@ -1,6 +1,8 @@ package com.topjohnwu.magisk.extensions +import android.content.ComponentName import android.content.Context +import android.content.ContextWrapper import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.ComponentInfo @@ -8,13 +10,23 @@ import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.* import android.content.res.Configuration +import android.content.res.Resources import android.database.Cursor import android.net.Uri +import android.os.Build import android.provider.OpenableColumns +import android.view.View +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import com.topjohnwu.magisk.utils.DynamicClassLoader import com.topjohnwu.magisk.utils.FileProvider +import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.currentLocale import java.io.File import java.io.FileNotFoundException +import java.util.* val packageName: String get() = get().packageName @@ -85,6 +97,105 @@ fun Context.readUri(uri: Uri) = fun Intent.startActivity(context: Context) = context.startActivity(this) +fun Intent.toCommand(args: MutableList) { + if (action != null) { + args.add("-a") + args.add(action!!) + } + if (component != null) { + args.add("-n") + args.add(component!!.flattenToString()) + } + if (data != null) { + args.add("-d") + args.add(dataString!!) + } + if (categories != null) { + for (cat in categories) { + args.add("-c") + args.add(cat) + } + } + if (type != null) { + args.add("-t") + args.add(type!!) + } + val extras = extras + if (extras != null) { + loop@ for (key in extras.keySet()) { + val v = extras.get(key) ?: continue + var value: Any = v + val arg: String + when { + v is String -> arg = "--es" + v is Boolean -> arg = "--ez" + v is Int -> arg = "--ei" + v is Long -> arg = "--el" + v is Float -> arg = "--ef" + v is Uri -> arg = "--eu" + v is ComponentName -> { + arg = "--ecn" + value = v.flattenToString() + } + v is ArrayList<*> -> { + if (v.size <= 0) + /* Impossible to know the type due to type erasure */ + continue@loop + + arg = if (v[0] is Int) + "--eial" + else if (v[0] is Long) + "--elal" + else if (v[0] is Float) + "--efal" + else if (v[0] is String) + "--esal" + else + continue@loop /* Unsupported */ + + val sb = StringBuilder() + for (o in v) { + sb.append(o.toString().replace(",", "\\,")) + sb.append(',') + } + // Remove trailing comma + sb.deleteCharAt(sb.length - 1) + value = sb + } + v.javaClass.isArray -> { + arg = if (v is IntArray) + "--eia" + else if (v is LongArray) + "--ela" + else if (v is FloatArray) + "--efa" + else if (v is Array<*> && v.isArrayOf()) + "--esa" + else + continue@loop /* Unsupported */ + + val sb = StringBuilder() + val len = java.lang.reflect.Array.getLength(v) + for (i in 0 until len) { + sb.append(java.lang.reflect.Array.get(v, i)!!.toString().replace(",", "\\,")) + sb.append(',') + } + // Remove trailing comma + sb.deleteCharAt(sb.length - 1) + value = sb + } + else -> continue@loop + } /* Unsupported */ + + args.add(arg) + args.add(key) + args.add(value.toString()) + } + } + args.add("-f") + args.add(flags.toString()) +} + fun File.provide(context: Context = get()): Uri { return FileProvider.getUriForFile(context, context.packageName + ".provider", this) } @@ -119,3 +230,48 @@ fun ApplicationInfo.getLabel(pm: PackageManager): String { return loadLabel(pm).toString() } + +fun Intent.exists(packageManager: PackageManager) = resolveActivity(packageManager) != null + +fun Context.colorCompat(@ColorRes id: Int) = try { + ContextCompat.getColor(this, id) +} catch (e: Resources.NotFoundException) { + null +} + +fun Context.colorStateListCompat(@ColorRes id: Int) = try { + ContextCompat.getColorStateList(this, id) +} catch (e: Resources.NotFoundException) { + null +} + +fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.getDrawable(this, id) +/** + * Pass [start] and [end] dimensions, function will return left and right + * 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 && + resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL + ) { + return end to start + } + return start to end +} + +fun Context.openUrl(url: String) = Utils.openLink(this, url.toUri()) + +@Suppress("FunctionName") +inline fun T.DynamicClassLoader(apk: File) + = DynamicClassLoader(apk, T::class.java.classLoader) + +fun Context.unwrap() : Context { + var context = this + while (true) { + if (context is ContextWrapper) + context = context.baseContext + else + break + } + return context +} diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/XBinding.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/XBinding.kt index 36874f1e4..c0c5c8dd1 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/XBinding.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/XBinding.kt @@ -1,6 +1,6 @@ package com.topjohnwu.magisk.extensions -import com.skoumal.teanity.util.KObservableField +import com.topjohnwu.magisk.utils.KObservableField fun KObservableField.toggle() { diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt index ee79f13f8..88e5807c1 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt @@ -2,8 +2,7 @@ package com.topjohnwu.magisk.extensions import androidx.collection.SparseArrayCompat import androidx.databinding.ObservableList -import com.skoumal.teanity.extensions.subscribeK -import com.skoumal.teanity.util.DiffObservableList +import com.topjohnwu.magisk.utils.DiffObservableList import io.reactivex.disposables.Disposable fun MutableList.update(newList: List) { @@ -26,8 +25,8 @@ fun List.toShellCmd(): String { } fun ObservableList.sendUpdatesTo( - target: DiffObservableList, - mapper: (List) -> List + target: DiffObservableList, + mapper: (List) -> List ) = addOnListChangedCallback(object : ObservableList.OnListChangedCallback>() { override fun onChanged(sender: ObservableList?) { diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/XRx.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/XRx.kt deleted file mode 100644 index 7dbb2bd08..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/XRx.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.topjohnwu.magisk.extensions - -import io.reactivex.Single -import io.reactivex.functions.BiFunction - -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) }) \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/binding/BindingAdapter.kt b/app/src/main/java/com/topjohnwu/magisk/model/binding/BindingAdapter.kt index 5bfad68c6..ec4cd43b2 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/binding/BindingAdapter.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/binding/BindingAdapter.kt @@ -2,7 +2,7 @@ package com.topjohnwu.magisk.model.binding import androidx.databinding.ViewDataBinding import androidx.recyclerview.widget.RecyclerView -import com.skoumal.teanity.databinding.ComparableRvItem +import com.topjohnwu.magisk.databinding.ComparableRvItem import com.topjohnwu.magisk.model.entity.recycler.LenientRvItem import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter diff --git a/app/src/main/java/com/topjohnwu/magisk/model/download/DownloadService.kt b/app/src/main/java/com/topjohnwu/magisk/model/download/DownloadService.kt index 4e2c2792e..4fbf8c456 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/download/DownloadService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/download/DownloadService.kt @@ -10,6 +10,7 @@ import androidx.core.app.NotificationCompat import com.topjohnwu.magisk.ClassMap import com.topjohnwu.magisk.R import com.topjohnwu.magisk.extensions.chooser +import com.topjohnwu.magisk.extensions.exists import com.topjohnwu.magisk.extensions.provide import com.topjohnwu.magisk.model.entity.internal.Configuration.* import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary @@ -17,6 +18,7 @@ import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.* import com.topjohnwu.magisk.ui.flash.FlashActivity import com.topjohnwu.magisk.utils.APKInstall +import org.koin.core.get import java.io.File import kotlin.random.Random.Default.nextInt @@ -76,8 +78,14 @@ open class DownloadService : RemoteFileService() { private fun NotificationCompat.Builder.addActionsInternal(subject: Magisk) = when (val conf = subject.configuration) { - Download -> addAction(0, R.string.download_open_parent, fileIntent(subject.file.parentFile!!)) - .addAction(0, R.string.download_open_self, fileIntent(subject.file)) + Download -> this.apply { + fileIntent(subject.file.parentFile!!) + .takeIf { it.exists(get()) } + ?.let { addAction(0, R.string.download_open_parent, it.chooser()) } + fileIntent(subject.file) + .takeIf { it.exists(get()) } + ?.let { addAction(0, R.string.download_open_self, it.chooser()) } + } Uninstall -> setContentIntent(FlashActivity.uninstallIntent(context, subject.file)) is Flash -> setContentIntent(FlashActivity.flashIntent(context, subject.file, conf is Secondary)) is Patch -> setContentIntent(FlashActivity.patchIntent(context, subject.file, conf.fileUri)) @@ -86,8 +94,14 @@ open class DownloadService : RemoteFileService() { private fun NotificationCompat.Builder.addActionsInternal(subject: Module) = when (subject.configuration) { - Download -> addAction(0, R.string.download_open_parent, fileIntent(subject.file.parentFile!!)) - .addAction(0, R.string.download_open_self, fileIntent(subject.file)) + Download -> this.apply { + fileIntent(subject.file.parentFile!!) + .takeIf { it.exists(get()) } + ?.let { addAction(0, R.string.download_open_parent, it.chooser()) } + fileIntent(subject.file) + .takeIf { it.exists(get()) } + ?.let { addAction(0, R.string.download_open_self, it.chooser()) } + } is Flash -> setContentIntent(FlashActivity.installIntent(context, subject.file)) else -> this } @@ -115,7 +129,6 @@ open class DownloadService : RemoteFileService() { .setDataAndType(file.provide(this), file.type) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .chooser() } class Builder { diff --git a/app/src/main/java/com/topjohnwu/magisk/model/download/ManagerUpgrade.kt b/app/src/main/java/com/topjohnwu/magisk/model/download/ManagerUpgrade.kt index 4fdf3b9fe..278edafb0 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/download/ManagerUpgrade.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/download/ManagerUpgrade.kt @@ -5,13 +5,13 @@ import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.ClassMap import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.extensions.DynamicClassLoader import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Restore import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Upgrade import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.ui.SplashActivity -import com.topjohnwu.magisk.utils.DynamicClassLoader import com.topjohnwu.magisk.utils.PatchAPK -import com.topjohnwu.magisk.utils.RootUtils +import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.superuser.Shell import timber.log.Timber import java.io.File @@ -54,7 +54,7 @@ private fun RemoteFileService.restore(apk: File, id: Int) { if (Shell.su("pm install $apk").exec().isSuccess) { val component = ComponentName(BuildConfig.APPLICATION_ID, ClassMap.get>(SplashActivity::class.java).name) - RootUtils.rmAndLaunch(packageName, component) + Utils.rmAndLaunch(packageName, component) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/download/NotificationService.kt b/app/src/main/java/com/topjohnwu/magisk/model/download/NotificationService.kt index 41d8eede0..f558265e3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/download/NotificationService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/download/NotificationService.kt @@ -1,15 +1,16 @@ package com.topjohnwu.magisk.model.download import android.app.Notification -import android.app.Service import android.content.Intent import android.os.IBinder import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import com.topjohnwu.magisk.base.BaseService +import org.koin.core.KoinComponent import java.util.* import kotlin.random.Random.Default.nextInt -abstract class NotificationService : Service() { +abstract class NotificationService : BaseService(), KoinComponent { abstract val defaultNotification: NotificationCompat.Builder diff --git a/app/src/main/java/com/topjohnwu/magisk/model/download/RemoteFileService.kt b/app/src/main/java/com/topjohnwu/magisk/model/download/RemoteFileService.kt index 84b5a6742..1c5fea005 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/download/RemoteFileService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/download/RemoteFileService.kt @@ -3,11 +3,11 @@ package com.topjohnwu.magisk.model.download import android.app.Activity import android.content.Intent import androidx.core.app.NotificationCompat -import com.skoumal.teanity.extensions.subscribeK import com.topjohnwu.magisk.R import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.di.NullActivity import com.topjohnwu.magisk.extensions.get +import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.extensions.writeTo import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.* diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/Version.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/Version.kt deleted file mode 100644 index 5a7728a15..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/Version.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.topjohnwu.magisk.model.entity - -data class Version(val version: String, val versionCode: Int) \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/module/Module.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/module/Module.kt index 8b7d5fa3b..f71e4c71d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/module/Module.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/module/Module.kt @@ -48,7 +48,7 @@ class Module(path: String) : BaseModule() { } if (name.isEmpty()) { - name = id; + name = id } } @@ -65,7 +65,7 @@ class Module(path: String) : BaseModule() { val module = Module(Const.MAGISK_PATH + "/" + file.name) moduleList.add(module) } - return moduleList + return moduleList.sortedBy { it.name } } } } 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 1ea743e92..bf7a0d2a3 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,17 +1,17 @@ package com.topjohnwu.magisk.model.entity.recycler -import com.skoumal.teanity.databinding.ComparableRvItem -import com.skoumal.teanity.extensions.addOnPropertyChangedCallback -import com.skoumal.teanity.rxbus.RxBus -import com.skoumal.teanity.util.DiffObservableList -import com.skoumal.teanity.util.KObservableField 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.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.utils.KObservableField +import com.topjohnwu.magisk.utils.RxBus class HideRvItem(val item: HideAppInfo, targets: List) : ComparableRvItem() { diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LenientRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LenientRvItem.kt index ddbd2fbec..f21df6b91 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LenientRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LenientRvItem.kt @@ -2,7 +2,7 @@ package com.topjohnwu.magisk.model.entity.recycler import androidx.databinding.ViewDataBinding import androidx.recyclerview.widget.RecyclerView -import com.skoumal.teanity.databinding.ComparableRvItem +import com.topjohnwu.magisk.databinding.ComparableRvItem /** * This item addresses issues where enclosing recycler has to be invalidated or generally 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 8f467b8fd..ad278ef80 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,14 +1,14 @@ package com.topjohnwu.magisk.model.entity.recycler -import com.skoumal.teanity.databinding.ComparableRvItem -import com.skoumal.teanity.util.DiffObservableList -import com.skoumal.teanity.util.KObservableField import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.databinding.ComparableRvItem import com.topjohnwu.magisk.extensions.timeFormatMedium 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 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 57caade72..1af52be7b 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 @@ -2,14 +2,14 @@ package com.topjohnwu.magisk.model.entity.recycler import android.content.res.Resources import androidx.annotation.StringRes -import com.skoumal.teanity.databinding.ComparableRvItem -import com.skoumal.teanity.extensions.addOnPropertyChangedCallback -import com.skoumal.teanity.util.KObservableField import com.topjohnwu.magisk.R +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.utils.KObservableField class ModuleRvItem(val item: Module) : ComparableRvItem() { 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 e72437fe6..f6610c363 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 @@ -1,16 +1,16 @@ package com.topjohnwu.magisk.model.entity.recycler import android.graphics.drawable.Drawable -import com.skoumal.teanity.databinding.ComparableRvItem -import com.skoumal.teanity.extensions.addOnPropertyChangedCallback -import com.skoumal.teanity.rxbus.RxBus -import com.skoumal.teanity.util.KObservableField 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.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.utils.KObservableField +import com.topjohnwu.magisk.utils.RxBus class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem() { diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SectionRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SectionRvItem.kt index 11fd9ebe3..9aad6d59e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SectionRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SectionRvItem.kt @@ -1,7 +1,7 @@ package com.topjohnwu.magisk.model.entity.recycler -import com.skoumal.teanity.databinding.ComparableRvItem import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.databinding.ComparableRvItem class SectionRvItem(val text: String) : ComparableRvItem() { override val layoutRes: Int = R.layout.item_section diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SpinnerRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SpinnerRvItem.kt index 92dcbf08f..7a69f924e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SpinnerRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SpinnerRvItem.kt @@ -1,7 +1,7 @@ package com.topjohnwu.magisk.model.entity.recycler -import com.skoumal.teanity.databinding.ComparableRvItem import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.databinding.ComparableRvItem class SpinnerRvItem(val item: String) : ComparableRvItem() { diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/EventHandler.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/EventHandler.kt new file mode 100644 index 000000000..fc0b4fc62 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/EventHandler.kt @@ -0,0 +1,25 @@ +package com.topjohnwu.magisk.model.events + +internal interface EventHandler { + + /** + * Called for all [ViewEvent]s published by associated viewModel. + * For [SimpleViewEvent]s, both this and [onSimpleEventDispatched] + * methods are called - you can choose the way how you handle them. + */ + fun onEventDispatched(event: ViewEvent) {} + + /** + * Called for all [SimpleViewEvent]s published by associated viewModel. + * Both this and [onEventDispatched] methods are called - you can choose + * the way how you handle them. + */ + fun onSimpleEventDispatched(event: Int) {} + + val viewEventObserver get() = ViewEventObserver { + onEventDispatched(it) + if (it is SimpleViewEvent) { + onSimpleEventDispatched(it.event) + } + } +} 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 5f6335bfe..a0dd317e4 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,10 +1,10 @@ package com.topjohnwu.magisk.model.events -import com.skoumal.teanity.rxbus.RxBus 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.utils.RxBus class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/SimpleViewEvent.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/SimpleViewEvent.kt new file mode 100644 index 000000000..4b8c70a87 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/SimpleViewEvent.kt @@ -0,0 +1,5 @@ +package com.topjohnwu.magisk.model.events + +class SimpleViewEvent( + val event: Int +) : ViewEvent() \ No newline at end of file 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 new file mode 100644 index 000000000..c60d79835 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/SnackbarEvent.kt @@ -0,0 +1,27 @@ +package com.topjohnwu.magisk.model.events + +import android.content.Context +import androidx.annotation.StringRes +import com.google.android.material.snackbar.Snackbar + +class SnackbarEvent private constructor( + @StringRes private val messageRes: Int, + private val messageString: String?, + val length: Int, + val f: Snackbar.() -> Unit +) : ViewEvent() { + + constructor( + @StringRes messageRes: Int, + length: Int = Snackbar.LENGTH_SHORT, + f: Snackbar.() -> Unit = {} + ) : this(messageRes, null, length, f) + + constructor( + message: String, + length: Int = Snackbar.LENGTH_SHORT, + f: Snackbar.() -> Unit = {} + ) : this(-1, message, length, f) + + fun message(context: Context): String = messageString ?: context.getString(messageRes) +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEventObserver.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEventObserver.kt new file mode 100644 index 000000000..7f98f966b --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEventObserver.kt @@ -0,0 +1,17 @@ +package com.topjohnwu.magisk.model.events + +import androidx.lifecycle.Observer + +/** + * Observer for [ViewEvent]s, which automatically checks if event was handled + */ +class ViewEventObserver(private val onEventUnhandled: (ViewEvent) -> Unit) : Observer { + override fun onChanged(event: ViewEvent?) { + event?.let { + if (!it.handled) { + it.handled = true + onEventUnhandled(it) + } + } + } +} \ No newline at end of file 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 b9a5ebd64..02062c62b 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 @@ -12,6 +12,16 @@ import com.topjohnwu.magisk.model.entity.module.Repo import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder import io.reactivex.subjects.PublishSubject +/** + * Class for passing events from ViewModels to Activities/Fragments + * Variable [handled] used so each event is handled only once + * (see https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150) + * Use [ViewEventObserver] for observing these events + */ +abstract class ViewEvent { + + var handled = false +} data class OpenLinkEvent(val url: String) : ViewEvent() @@ -76,4 +86,4 @@ class BackPressEvent : ViewEvent(), ActivityExecutor { } } -class DieEvent : ViewEvent() \ No newline at end of file +class DieEvent : ViewEvent() 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 89be3ea61..5a84deaec 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 @@ -5,12 +5,14 @@ import androidx.annotation.AnimRes import androidx.annotation.AnimatorRes import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment -import com.skoumal.teanity.viewevents.NavigationDslMarker -import com.skoumal.teanity.viewevents.ViewEvent +import com.topjohnwu.magisk.model.events.ViewEvent import com.topjohnwu.magisk.model.events.ActivityExecutor import com.topjohnwu.magisk.redesign.compat.CompatActivity import kotlin.reflect.KClass +@DslMarker +annotation class NavigationDslMarker + class MagiskNavigationEvent( val navDirections: MagiskNavDirectionsBuilder, val navOptions: MagiskNavOptions, diff --git a/app/src/main/java/com/topjohnwu/magisk/model/receiver/GeneralReceiver.kt b/app/src/main/java/com/topjohnwu/magisk/model/receiver/GeneralReceiver.kt index 93e277780..587712370 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/receiver/GeneralReceiver.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/receiver/GeneralReceiver.kt @@ -1,15 +1,14 @@ package com.topjohnwu.magisk.model.receiver -import android.content.BroadcastReceiver -import android.content.Context +import android.content.ContextWrapper import android.content.Intent import com.topjohnwu.magisk.ClassMap 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.data.database.base.su -import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.magisk.extensions.reboot import com.topjohnwu.magisk.model.download.DownloadService import com.topjohnwu.magisk.model.entity.ManagerJson @@ -20,8 +19,9 @@ import com.topjohnwu.magisk.utils.SuLogger import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.superuser.Shell +import org.koin.core.inject -open class GeneralReceiver : BroadcastReceiver() { +open class GeneralReceiver : BaseReceiver() { private val policyDB: PolicyDao by inject() @@ -36,7 +36,7 @@ open class GeneralReceiver : BroadcastReceiver() { return intent.data?.encodedSchemeSpecificPart.orEmpty() } - override fun onReceive(context: Context, intent: Intent?) { + override fun onReceive(context: ContextWrapper, intent: Intent?) { intent ?: return when (intent.action ?: return) { Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> { diff --git a/app/src/main/java/com/topjohnwu/magisk/model/update/UpdateCheckService.kt b/app/src/main/java/com/topjohnwu/magisk/model/update/UpdateCheckService.kt index 68c3043a6..17841cd0c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/update/UpdateCheckService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/update/UpdateCheckService.kt @@ -3,9 +3,9 @@ package com.topjohnwu.magisk.model.update import androidx.work.ListenableWorker import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.Info +import com.topjohnwu.magisk.base.DelegateWorker import com.topjohnwu.magisk.data.repository.MagiskRepository import com.topjohnwu.magisk.extensions.inject -import com.topjohnwu.magisk.model.worker.DelegateWorker import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.superuser.Shell diff --git a/app/src/main/java/com/topjohnwu/magisk/tasks/FlashZip.kt b/app/src/main/java/com/topjohnwu/magisk/tasks/FlashZip.kt index 3ae10b1a3..2e9e9ac36 100644 --- a/app/src/main/java/com/topjohnwu/magisk/tasks/FlashZip.kt +++ b/app/src/main/java/com/topjohnwu/magisk/tasks/FlashZip.kt @@ -2,11 +2,11 @@ package com.topjohnwu.magisk.tasks import android.content.Context import android.net.Uri -import com.skoumal.teanity.extensions.subscribeK import com.topjohnwu.magisk.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.superuser.Shell import io.reactivex.Single diff --git a/app/src/main/java/com/topjohnwu/magisk/tasks/MagiskInstaller.kt b/app/src/main/java/com/topjohnwu/magisk/tasks/MagiskInstaller.kt index 9ef1c8d72..5357f9703 100644 --- a/app/src/main/java/com/topjohnwu/magisk/tasks/MagiskInstaller.kt +++ b/app/src/main/java/com/topjohnwu/magisk/tasks/MagiskInstaller.kt @@ -6,7 +6,6 @@ import android.os.Build import android.text.TextUtils import androidx.annotation.MainThread import androidx.annotation.WorkerThread -import com.skoumal.teanity.extensions.subscribeK import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Info import com.topjohnwu.magisk.data.network.GithubRawServices @@ -25,9 +24,11 @@ import org.kamranzafar.jtar.TarHeader import org.kamranzafar.jtar.TarInputStream import org.kamranzafar.jtar.TarOutputStream import timber.log.Timber -import java.io.* +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream import java.nio.ByteBuffer -import java.util.* import java.util.zip.ZipEntry import java.util.zip.ZipInputStream @@ -40,7 +41,7 @@ abstract class MagiskInstaller { private val console: MutableList private val logs: MutableList - private var isTar = false + private var tarOut: TarOutputStream? = null private val service: GithubRawServices by inject() private val context: Context by inject() @@ -151,7 +152,9 @@ abstract class MagiskInstaller { private fun handleTar(input: InputStream) { console.add("- Processing tar file") var vbmeta = false - withStreams(TarInputStream(input), TarOutputStream(destFile)) { tarIn, tarOut -> + val tarOut = TarOutputStream(destFile) + this.tarOut = tarOut + TarInputStream(input).use { tarIn -> lateinit var entry: TarEntry while (tarIn.nextEntry?.let { entry = it } != null) { if (entry.name.contains("boot.img") || entry.name.contains("recovery.img")) { @@ -215,8 +218,7 @@ abstract class MagiskInstaller { return false } it.reset() - if (Arrays.equals(magic, "ustar".toByteArray())) { - isTar = true + if (magic.contentEquals("ustar".toByteArray())) { destFile = File(Config.downloadDirectory, "magisk_patched.tar") handleTar(it) } else { @@ -293,15 +295,13 @@ abstract class MagiskInstaller { protected fun storeBoot(): Boolean { val patched = SuFile.open(installDir, "new-boot.img") try { - val os: OutputStream - if (isTar) { - os = TarOutputStream(destFile, true) - os.putNextEntry(newEntry( + val os = tarOut?.let { + it.putNextEntry(newEntry( if (srcBoot.contains("recovery")) "recovery.img" else "boot.img", patched.length())) - } else { - os = destFile.outputStream() - } + tarOut = null + it + } ?: destFile.outputStream() patched.suInputStream().use { it.copyTo(os); os.close() } } catch (e: IOException) { console.add("! Failed to output to $destFile") 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 86d13c915..848192ca6 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt @@ -4,15 +4,24 @@ import android.content.Intent import android.os.Bundle import androidx.core.view.GravityCompat import androidx.fragment.app.Fragment -import com.skoumal.teanity.extensions.addOnPropertyChangedCallback +import androidx.fragment.app.FragmentTransaction +import com.ncapdevi.fragnav.FragNavController +import com.ncapdevi.fragnav.FragNavTransactionOptions import com.topjohnwu.magisk.ClassMap import com.topjohnwu.magisk.Config 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.model.events.* +import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder +import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent import com.topjohnwu.magisk.model.navigation.Navigation -import com.topjohnwu.magisk.ui.base.MagiskActivity +import com.topjohnwu.magisk.model.navigation.Navigator import com.topjohnwu.magisk.ui.hide.MagiskHideFragment import com.topjohnwu.magisk.ui.home.HomeFragment import com.topjohnwu.magisk.ui.log.LogFragment @@ -23,15 +32,22 @@ import com.topjohnwu.magisk.ui.superuser.SuperuserFragment import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.superuser.Shell import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber import kotlin.reflect.KClass - -open class MainActivity : MagiskActivity() { +open class MainActivity : BaseActivity(), Navigator, + FragNavController.RootFragmentListener, FragNavController.TransactionListener { override val layoutRes: Int = R.layout.activity_main override val viewModel: MainViewModel by viewModel() - override val navHostId: Int = R.id.main_nav_host - override val defaultPosition: Int = 0 + private val navHostId: Int = R.id.main_nav_host + private val defaultPosition: Int = 0 + + private val navigationController by lazy { + FragNavController(supportFragmentManager, navHostId) + } + private val isRootFragment get() = + navigationController.currentStackIndex != defaultPosition override val baseFragments: List> = listOf( HomeFragment::class, @@ -43,10 +59,6 @@ open class MainActivity : MagiskActivity() { SettingsFragment::class ) - /*override fun getDarkTheme(): Int { - return R.style.AppTheme_Dark - }*/ - override fun onCreate(savedInstanceState: Bundle?) { if (!SplashActivity.DONE) { startActivity(Intent(this, ClassMap[SplashActivity::class.java])) @@ -54,6 +66,13 @@ open class MainActivity : MagiskActivity() { } super.onCreate(savedInstanceState) + + navigationController.apply { + rootFragmentListener = this@MainActivity + transactionListener = this@MainActivity + initialize(defaultPosition, savedInstanceState) + } + checkHideSection() setSupportActionBar(binding.mainInclude.mainToolbar) @@ -68,6 +87,11 @@ open class MainActivity : MagiskActivity() { } } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + navigationController.onSaveInstanceState(outState) + } + override fun setTitle(title: CharSequence?) { supportActionBar?.title = title } @@ -76,25 +100,46 @@ open class MainActivity : MagiskActivity() { supportActionBar?.setTitle(titleId) } - 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 onBackPressed() { if (binding.drawerLayout.isDrawerOpen(binding.navView)) { binding.drawerLayout.closeDrawer(binding.navView) } else { - super.onBackPressed() + 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 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")) + } + } } } @@ -120,4 +165,84 @@ open class MainActivity : MagiskActivity() { 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 onFragmentTransaction( + fragment: Fragment?, + transactionType: FragNavController.TransactionType + ) = Unit } 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 73d8b80c8..35688183f 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt @@ -2,11 +2,11 @@ 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.MagiskViewModel -class MainViewModel : MagiskViewModel() { +class MainViewModel : BaseViewModel() { fun navPressed() = Navigation.Main.OPEN_NAV.publish() diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt index 83bb66ec4..a2c8201b3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt @@ -1,17 +1,24 @@ package com.topjohnwu.magisk.ui +import android.app.Activity +import android.content.Context +import android.content.Intent import android.os.Bundle import android.text.TextUtils import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity import com.topjohnwu.magisk.* import com.topjohnwu.magisk.model.navigation.Navigation import com.topjohnwu.magisk.utils.Utils +import com.topjohnwu.magisk.utils.wrap import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.superuser.Shell -open class SplashActivity : AppCompatActivity() { +open class SplashActivity : Activity() { + + override fun attachBaseContext(base: Context) { + super.attachBaseContext(base.wrap()) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/BasePreferenceFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/BasePreferenceFragment.kt deleted file mode 100644 index 2642494cd..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/BasePreferenceFragment.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.topjohnwu.magisk.ui.base - -import android.annotation.SuppressLint -import android.content.SharedPreferences -import android.os.Build -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.children -import androidx.core.view.isVisible -import androidx.preference.* -import androidx.recyclerview.widget.RecyclerView -import com.topjohnwu.magisk.R -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 MagiskActivity<*, *> - - 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() - } - - override fun onCreateAdapter(preferenceScreen: PreferenceScreen): RecyclerView.Adapter<*> { - return object : PreferenceGroupAdapter(preferenceScreen) { - @SuppressLint("RestrictedApi") - override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - when (val preference = getItem(position)) { - is PreferenceCategory -> setZeroPaddingToLayoutChildren(holder.itemView) - else -> holder.itemView.findViewById(R.id.icon_frame)?.isVisible = - preference.icon != null - } - } - } - } - - private fun setZeroPaddingToLayoutChildren(view: View) { - (view as? ViewGroup)?.children?.forEach { - setZeroPaddingToLayoutChildren(it) - } ?: return - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) - view.setPaddingRelative(0, view.paddingTop, view.paddingEnd, view.paddingBottom) - else - view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom) - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt deleted file mode 100644 index f47d314db..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt +++ /dev/null @@ -1,242 +0,0 @@ -package com.topjohnwu.magisk.ui.base - -import android.Manifest -import android.content.Context -import android.content.Intent -import android.content.res.Configuration -import android.os.Bundle -import androidx.annotation.CallSuper -import androidx.appcompat.app.AppCompatDelegate -import androidx.collection.SparseArrayCompat -import androidx.core.net.toUri -import androidx.databinding.ViewDataBinding -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentTransaction -import com.karumi.dexter.Dexter -import com.karumi.dexter.MultiplePermissionsReport -import com.karumi.dexter.PermissionToken -import com.karumi.dexter.listener.PermissionRequest -import com.karumi.dexter.listener.multi.MultiplePermissionsListener -import com.ncapdevi.fragnav.FragNavController -import com.ncapdevi.fragnav.FragNavTransactionOptions -import com.skoumal.teanity.view.TeanityActivity -import com.skoumal.teanity.viewevents.ViewEvent -import com.topjohnwu.magisk.Config -import com.topjohnwu.magisk.extensions.set -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.model.navigation.MagiskAnimBuilder -import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent -import com.topjohnwu.magisk.model.navigation.Navigator -import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder -import com.topjohnwu.magisk.utils.LocaleManager -import com.topjohnwu.magisk.utils.Utils -import com.topjohnwu.magisk.utils.currentLocale -import timber.log.Timber -import kotlin.reflect.KClass - -typealias RequestCallback = MagiskActivity<*, *>.(Int, Intent?) -> Unit - -abstract class MagiskActivity : - TeanityActivity(), FragNavController.RootFragmentListener, - Navigator, FragNavController.TransactionListener { - - override val numberOfRootFragments: Int get() = baseFragments.size - override val baseFragments: List> = listOf() - private val resultCallbacks = SparseArrayCompat() - - - protected open val defaultPosition: Int = 0 - - protected val navigationController get() = if (navHostId == 0) null else _navigationController - private val _navigationController by lazy { - if (navHostId == 0) throw IllegalStateException("Did you forget to override \"navHostId\"?") - FragNavController(supportFragmentManager, navHostId) - } - - private val isRootFragment - get() = navigationController?.let { it.currentStackIndex != defaultPosition } ?: false - - 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) - super.applyOverrideConfiguration(config) - } - - override fun attachBaseContext(base: Context) { - super.attachBaseContext(LocaleManager.getLocaleContext(base)) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - navigationController?.apply { - rootFragmentListener = this@MagiskActivity - transactionListener = this@MagiskActivity - initialize(defaultPosition, savedInstanceState) - } - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - navigationController?.onSaveInstanceState(outState) - } - - @CallSuper - override fun onEventDispatched(event: ViewEvent) { - super.onEventDispatched(event) - when (event) { - 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 getRootFragment(index: Int) = baseFragments[index].java.newInstance() - - 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 onBackPressed() { - val fragment = navigationController?.currentFrag as? MagiskFragment<*, *> - - if (fragment?.onBackPressed() == true) { - return - } - - try { - navigationController?.popFragment() ?: throw UnsupportedOperationException() - } catch (e: UnsupportedOperationException) { - when { - isRootFragment -> { - val options = FragNavTransactionOptions.newBuilder() - .transition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE) - .build() - navigationController?.switchTab(defaultPosition, options) - } - else -> super.onBackPressed() - } - } - } - - override fun onFragmentTransaction( - fragment: Fragment?, - transactionType: FragNavController.TransactionType - ) = Unit - - override fun onTabTransaction(fragment: Fragment?, index: Int) = Unit - - fun openUrl(url: String) = Utils.openLink(this, url.toUri()) - - fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) { - val request = PermissionRequestBuilder().apply(builder).build() - Dexter.withActivity(this) - .withPermissions(*permissions) - .withListener(object : MultiplePermissionsListener { - override fun onPermissionsChecked(report: MultiplePermissionsReport) { - if (report.areAllPermissionsGranted()) { - request.onSuccess() - } else { - request.onFailure() - } - } - - override fun onPermissionRationaleShouldBeShown( - permissions: MutableList, - token: PermissionToken - ) = token.continuePermissionRequest() - }).check() - } - - fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) { - withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder) - } - - private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) = - customAnimations(options.enter, options.exit, options.popEnter, options.popExit).apply { - if (!options.anySet) { - transition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - resultCallbacks[requestCode]?.apply { - resultCallbacks.remove(requestCode) - invoke(this@MagiskActivity, resultCode, data) - } - } - - fun startActivityForResult( - intent: Intent, - requestCode: Int, - listener: RequestCallback - ) { - resultCallbacks[requestCode] = listener - startActivityForResult(intent, requestCode) - } - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt deleted file mode 100644 index f089ddff7..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.topjohnwu.magisk.ui.base - -import androidx.annotation.CallSuper -import androidx.databinding.ViewDataBinding -import androidx.fragment.app.Fragment -import com.skoumal.teanity.view.TeanityFragment -import com.skoumal.teanity.viewevents.ViewEvent -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.model.navigation.MagiskNavigationEvent -import com.topjohnwu.magisk.model.navigation.Navigator -import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder -import kotlin.reflect.KClass - - -abstract class MagiskFragment : - TeanityFragment(), Navigator { - - protected val activity get() = requireActivity() as MagiskActivity<*, *> - - // We don't need nested fragments - override val baseFragments: List> = listOf() - - override fun navigateTo(event: MagiskNavigationEvent) = activity.navigateTo(event) - - @CallSuper - override fun onEventDispatched(event: ViewEvent) { - super.onEventDispatched(event) - when (event) { - is BackPressEvent -> activity.onBackPressed() - is MagiskNavigationEvent -> navigateTo(event) - is ViewActionEvent -> event.action(requireActivity()) - is PermissionEvent -> activity.withPermissions(*event.permissions.toTypedArray()) { - onSuccess { event.callback.onNext(true) } - onFailure { - event.callback.onNext(false) - event.callback.onError(SecurityException("User refused permissions")) - } - } - } - } - - fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) { - activity.withPermissions(*permissions, builder = builder) - } - - fun openLink(url: String) = activity.openUrl(url) - - open fun onBackPressed(): Boolean = false - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.kt index d10deff8a..143db2a3e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.kt @@ -9,15 +9,21 @@ import androidx.core.net.toUri import com.topjohnwu.magisk.ClassMap import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.base.BaseActivity import com.topjohnwu.magisk.databinding.ActivityFlashBinding -import com.topjohnwu.magisk.ui.base.MagiskActivity +import com.topjohnwu.magisk.extensions.snackbar +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 org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import java.io.File -open class FlashActivity : MagiskActivity() { +open class FlashActivity : BaseActivity() { override val layoutRes: Int = R.layout.activity_flash + override val themeRes: Int = R.style.MagiskTheme_Flashing override val viewModel: FlashViewModel by viewModel { val uri = intent.data ?: let { finish(); Uri.EMPTY } val additionalUri = intent.getParcelableExtra(Const.Key.FLASH_DATA) ?: uri @@ -37,6 +43,21 @@ open class FlashActivity : MagiskActivity( super.onBackPressed() } + 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 PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) { + onSuccess { event.callback.onNext(true) } + onFailure { + event.callback.onNext(false) + event.callback.onError(SecurityException("User refused permissions")) + } + } + } + } + companion object { private fun intent(context: Context) = Intent(context, ClassMap[FlashActivity::class.java]) 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 92fe4210c..e2ead2b3b 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 @@ -7,21 +7,20 @@ import android.net.Uri import android.os.Handler import androidx.core.os.postDelayed import androidx.databinding.ObservableArrayList -import com.skoumal.teanity.databinding.ComparableRvItem -import com.skoumal.teanity.extensions.subscribeK -import com.skoumal.teanity.util.DiffObservableList -import com.skoumal.teanity.util.KObservableField -import com.skoumal.teanity.viewevents.SnackbarEvent 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.ui.base.MagiskViewModel +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 @@ -32,7 +31,7 @@ class FlashViewModel( installer: Uri, uri: Uri, private val resources: Resources -) : MagiskViewModel(), FlashResultListener { +) : BaseViewModel(), FlashResultListener { val canShowReboot = Shell.rootAccess() val showRestartTitle = KObservableField(false) 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 5e5a08992..8e09a010f 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,28 +1,28 @@ package com.topjohnwu.magisk.ui.hide import android.content.pm.ApplicationInfo -import com.skoumal.teanity.databinding.ComparableRvItem -import com.skoumal.teanity.extensions.addOnPropertyChangedCallback -import com.skoumal.teanity.extensions.subscribeK -import com.skoumal.teanity.rxbus.RxBus -import com.skoumal.teanity.util.DiffObservableList -import com.skoumal.teanity.util.KObservableField import com.topjohnwu.magisk.BR +import com.topjohnwu.magisk.base.viewmodel.BaseViewModel 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.ui.base.MagiskViewModel +import com.topjohnwu.magisk.utils.DiffObservableList +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 -) : MagiskViewModel() { +) : BaseViewModel() { val query = KObservableField("") val isShowSystem = KObservableField(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 index 3c0a14519..f9d4c67f3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/hide/MagiskHideFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/hide/MagiskHideFragment.kt @@ -6,11 +6,11 @@ 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 com.topjohnwu.magisk.ui.base.MagiskFragment import org.koin.androidx.viewmodel.ext.android.viewModel -class MagiskHideFragment : MagiskFragment(), +class MagiskHideFragment : BaseFragment(), SearchView.OnQueryTextListener { override val layoutRes: Int = R.layout.fragment_magisk_hide 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 3d5ca84fa..2d2b2161c 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,31 +1,31 @@ package com.topjohnwu.magisk.ui.home import android.content.Context -import com.skoumal.teanity.extensions.subscribeK -import com.skoumal.teanity.viewevents.ViewEvent import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.Const 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.data.repository.MagiskRepository import com.topjohnwu.magisk.databinding.FragmentMagiskBinding -import com.topjohnwu.magisk.extensions.inject +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.ui.base.MagiskActivity -import com.topjohnwu.magisk.ui.base.MagiskFragment -import com.topjohnwu.magisk.utils.DynamicClassLoader import com.topjohnwu.magisk.utils.SafetyNetHelper import com.topjohnwu.magisk.view.MarkDownWindow import com.topjohnwu.magisk.view.dialogs.* import com.topjohnwu.superuser.Shell import dalvik.system.DexFile import io.reactivex.Completable +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import java.io.File import java.lang.reflect.InvocationHandler -class HomeFragment : MagiskFragment(), +class HomeFragment : BaseFragment(), SafetyNetHelper.Callback { override val layoutRes: Int = R.layout.fragment_magisk @@ -33,13 +33,14 @@ class HomeFragment : MagiskFragment(), 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 -> openLink(event.url) + is OpenLinkEvent -> activity.openUrl(event.url) is ManagerInstallEvent -> installManager() is MagiskInstallEvent -> installMagisk() is UninstallEvent -> uninstall() @@ -62,7 +63,7 @@ class HomeFragment : MagiskFragment(), return } - MagiskInstallDialog(requireActivity() as MagiskActivity<*, *>).show() + MagiskInstallDialog(requireActivity() as BaseActivity<*, *>).show() } private fun installManager() = ManagerInstallDialog(requireActivity()).show() @@ -94,7 +95,7 @@ class HomeFragment : MagiskFragment(), private fun updateSafetyNet(dieOnError: Boolean) { Completable.fromAction { val loader = DynamicClassLoader(EXT_APK) - val dex = DexFile.loadDex(EXT_APK.path, EXT_APK.parent, 0) + val dex = DexFile.loadDex(EXT_APK.path, EXT_DEX.path, 0) // Scan through the dex and find our helper class var helperClass: Class<*>? = null 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 951b0948c..fd5da72b0 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,21 +1,16 @@ package com.topjohnwu.magisk.ui.home import android.content.pm.PackageManager -import com.skoumal.teanity.extensions.addOnPropertyChangedCallback -import com.skoumal.teanity.extensions.doOnSubscribeUi -import com.skoumal.teanity.extensions.subscribeK -import com.skoumal.teanity.util.KObservableField import com.topjohnwu.magisk.* +import com.topjohnwu.magisk.base.viewmodel.BaseViewModel import com.topjohnwu.magisk.data.repository.MagiskRepository -import com.topjohnwu.magisk.extensions.get -import com.topjohnwu.magisk.extensions.packageName -import com.topjohnwu.magisk.extensions.res -import com.topjohnwu.magisk.extensions.toggle +import com.topjohnwu.magisk.extensions.* import com.topjohnwu.magisk.model.events.* import com.topjohnwu.magisk.model.observer.Observer -import com.topjohnwu.magisk.ui.base.MagiskViewModel +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 @@ -31,7 +26,7 @@ enum class MagiskItem { class HomeViewModel( private val magiskRepo: MagiskRepository -) : MagiskViewModel(State.LOADED) { +) : BaseViewModel(State.LOADED) { val hasGMS = runCatching { get().getPackageInfo("com.google.android.gms", 0); true @@ -43,10 +38,7 @@ class HomeViewModel( val isKeepVerity = KObservableField(Info.keepVerity) val isRecovery = KObservableField(Info.recovery) - private val _magiskState = KObservableField(MagiskState.LOADING) - val magiskState = Observer(_magiskState, isConnected) { - if (isConnected.value) _magiskState.value else MagiskState.UP_TO_DATE - } + val magiskState = KObservableField(MagiskState.LOADING) val magiskStateText = Observer(magiskState) { when (magiskState.value) { MagiskState.NOT_INSTALLED -> R.string.magisk_version_error.res() @@ -179,24 +171,28 @@ class HomeViewModel( } fun refresh() { - refreshVersions() - - magiskRepo.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 - } - .subscribeK { - updateSelf() - ensureEnv() - refreshVersions() - } - hasRoot.value = Shell.rootAccess() + + val fetchUpdate = if (isConnected.value) + magiskRepo.fetchUpdate().ignoreElement() + else + Completable.complete() + + Completable.fromAction { + Info.loadMagiskInfo() + }.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 + }.subscribeK { + updateSelf() + ensureEnv() + refreshVersions() + } } private fun refreshVersions() { @@ -211,7 +207,7 @@ class HomeViewModel( } private fun updateSelf() { - _magiskState.value = when (Info.magiskVersionCode) { + magiskState.value = when (Info.magiskVersionCode) { in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED !in Info.remote.magisk.versionCode..Int.MAX_VALUE -> MagiskState.OBSOLETE else -> MagiskState.UP_TO_DATE 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 80c98c253..24f0e1db2 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 @@ -6,14 +6,14 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import com.skoumal.teanity.viewevents.ViewEvent 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.ui.base.MagiskFragment +import com.topjohnwu.magisk.model.events.ViewEvent import org.koin.androidx.viewmodel.ext.android.viewModel -class LogFragment : MagiskFragment() { +class LogFragment : BaseFragment() { override val layoutRes: Int = R.layout.fragment_log override val viewModel: LogViewModel by viewModel() 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 279ac5036..21beec611 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,25 +1,25 @@ package com.topjohnwu.magisk.ui.log import android.content.res.Resources -import com.skoumal.teanity.databinding.ComparableRvItem -import com.skoumal.teanity.extensions.addOnPropertyChangedCallback -import com.skoumal.teanity.extensions.doOnSubscribeUi -import com.skoumal.teanity.extensions.subscribeK -import com.skoumal.teanity.util.DiffObservableList -import com.skoumal.teanity.util.KObservableField -import com.skoumal.teanity.viewevents.SnackbarEvent 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.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.ui.base.MagiskViewModel +import com.topjohnwu.magisk.model.events.SnackbarEvent +import com.topjohnwu.magisk.utils.DiffObservableList +import com.topjohnwu.magisk.utils.KObservableField import com.topjohnwu.superuser.Shell import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter import me.tatarka.bindingcollectionadapter2.OnItemBind @@ -30,7 +30,7 @@ import java.util.* class LogViewModel( private val resources: Resources, private val logRepo: LogRepository -) : MagiskViewModel(), BindingViewPagerAdapter.PageTitles> { +) : BaseViewModel(), BindingViewPagerAdapter.PageTitles> { val itemsAdapter = BindingAdapter() val items = DiffObservableList(ComparableRvItem.callback) 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 f83af2c5a..1ac4ed62b 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,17 +1,12 @@ package com.topjohnwu.magisk.ui.module import android.content.res.Resources -import com.skoumal.teanity.databinding.ComparableRvItem -import com.skoumal.teanity.extensions.addOnPropertyChangedCallback -import com.skoumal.teanity.extensions.doOnSuccessUi -import com.skoumal.teanity.extensions.subscribeK -import com.skoumal.teanity.util.DiffObservableList -import com.skoumal.teanity.util.KObservableField 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.extensions.toSingle -import com.topjohnwu.magisk.extensions.update +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 @@ -20,7 +15,8 @@ import com.topjohnwu.magisk.model.events.InstallModuleEvent import com.topjohnwu.magisk.model.events.OpenChangelogEvent import com.topjohnwu.magisk.model.events.OpenFilePickerEvent import com.topjohnwu.magisk.tasks.RepoUpdater -import com.topjohnwu.magisk.ui.base.MagiskViewModel +import com.topjohnwu.magisk.utils.DiffObservableList +import com.topjohnwu.magisk.utils.KObservableField import io.reactivex.Single import io.reactivex.disposables.Disposable import me.tatarka.bindingcollectionadapter2.OnItemBind @@ -29,7 +25,7 @@ class ModuleViewModel( private val resources: Resources, private val repoUpdater: RepoUpdater, private val repoDB: RepoDao -) : MagiskViewModel() { +) : BaseViewModel() { val query = KObservableField("") 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 index 17a1e16fa..fa57d6605 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.kt @@ -8,19 +8,19 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.recyclerview.widget.RecyclerView -import com.skoumal.teanity.viewevents.ViewEvent import com.topjohnwu.magisk.ClassMap 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.model.events.OpenFilePickerEvent -import com.topjohnwu.magisk.ui.base.MagiskFragment +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 : MagiskFragment() { +class ModulesFragment : BaseFragment() { override val layoutRes: Int = R.layout.fragment_modules override val viewModel: ModuleViewModel by sharedViewModel() 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 index 6fa93d20a..70ebb78c5 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt @@ -6,9 +6,9 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.widget.SearchView -import com.skoumal.teanity.viewevents.ViewEvent 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 @@ -16,12 +16,12 @@ 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.ui.base.MagiskFragment +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 : MagiskFragment(), +class ReposFragment : BaseFragment(), SearchView.OnQueryTextListener { override val layoutRes: Int = R.layout.fragment_repos 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 cd03c6d37..67ca6f0bf 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 @@ -13,21 +13,20 @@ import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.SwitchPreferenceCompat -import com.skoumal.teanity.extensions.subscribeK -import com.skoumal.teanity.util.KObservableField 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.BasePreferenceFragment import com.topjohnwu.magisk.data.database.RepoDao import com.topjohnwu.magisk.databinding.CustomDownloadDialogBinding +import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.extensions.toLangTag 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.net.Networking -import com.topjohnwu.magisk.ui.base.BasePreferenceFragment import com.topjohnwu.magisk.utils.* import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog import com.topjohnwu.superuser.Shell @@ -204,7 +203,7 @@ class SettingsFragment : BasePreferenceFragment() { Shell.su("magiskhide --disable").submit() } Config.Key.LOCALE -> { - LocaleManager.setLocale(activity.application) + ResourceMgr.reload() activity.recreate() } Config.Key.CHECK_UPDATES -> Utils.scheduleUpdateCheck(activity) @@ -233,7 +232,7 @@ class SettingsFragment : BasePreferenceFragment() { val values = mutableListOf() names.add( - LocaleManager.getString(defaultLocale, R.string.system_default) + ResourceMgr.getString(defaultLocale, R.string.system_default) ) values.add("") 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 1547fec3e..00386b01e 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,12 +1,12 @@ package com.topjohnwu.magisk.ui.superuser import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.base.BaseFragment import com.topjohnwu.magisk.databinding.FragmentSuperuserBinding -import com.topjohnwu.magisk.ui.base.MagiskFragment import org.koin.androidx.viewmodel.ext.android.viewModel class SuperuserFragment : - MagiskFragment() { + BaseFragment() { override val layoutRes: Int = R.layout.fragment_superuser override val viewModel: SuperuserViewModel by viewModel() 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 954696716..adbae5577 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,22 +2,22 @@ package com.topjohnwu.magisk.ui.superuser import android.content.pm.PackageManager import android.content.res.Resources -import com.skoumal.teanity.databinding.ComparableRvItem -import com.skoumal.teanity.extensions.applySchedulers -import com.skoumal.teanity.extensions.subscribeK -import com.skoumal.teanity.rxbus.RxBus -import com.skoumal.teanity.util.DiffObservableList -import com.skoumal.teanity.viewevents.SnackbarEvent 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.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.events.PolicyUpdateEvent -import com.topjohnwu.magisk.ui.base.MagiskViewModel +import com.topjohnwu.magisk.model.events.SnackbarEvent +import com.topjohnwu.magisk.utils.DiffObservableList import com.topjohnwu.magisk.utils.FingerprintHelper +import com.topjohnwu.magisk.utils.RxBus import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog import io.reactivex.Single @@ -29,7 +29,7 @@ class SuperuserViewModel( private val packageManager: PackageManager, private val resources: Resources, rxBus: RxBus -) : MagiskViewModel() { +) : BaseViewModel() { val items = DiffObservableList(ComparableRvItem.callback) val itemBinding = ItemBinding.of> { itemBinding, _, item -> diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt index dc4f54b09..b55e108c2 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt @@ -5,19 +5,20 @@ import android.os.Build import android.os.Bundle import android.text.TextUtils import android.view.Window -import com.skoumal.teanity.viewevents.ViewEvent import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.base.BaseActivity import com.topjohnwu.magisk.databinding.ActivityRequestBinding import com.topjohnwu.magisk.model.entity.MagiskPolicy import com.topjohnwu.magisk.model.events.DieEvent +import com.topjohnwu.magisk.model.events.ViewEvent import com.topjohnwu.magisk.model.receiver.GeneralReceiver -import com.topjohnwu.magisk.ui.base.MagiskActivity import com.topjohnwu.magisk.utils.SuLogger import org.koin.androidx.viewmodel.ext.android.viewModel -open class SuRequestActivity : MagiskActivity() { +open class SuRequestActivity : BaseActivity() { override val layoutRes: Int = R.layout.activity_request + override val themeRes: Int = R.style.MagiskTheme_SU override val viewModel: SuRequestViewModel by viewModel() override fun onBackPressed() { 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 index 6cb43d425..0475b2104 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt @@ -9,21 +9,21 @@ import android.graphics.drawable.Drawable import android.hardware.fingerprint.FingerprintManager import android.os.CountDownTimer import android.text.TextUtils -import com.skoumal.teanity.databinding.ComparableRvItem -import com.skoumal.teanity.extensions.addOnPropertyChangedCallback -import com.skoumal.teanity.util.DiffObservableList -import com.skoumal.teanity.util.KObservableField import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.Config 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.addOnPropertyChangedCallback 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.ui.base.MagiskViewModel +import com.topjohnwu.magisk.utils.DiffObservableList import com.topjohnwu.magisk.utils.FingerprintHelper +import com.topjohnwu.magisk.utils.KObservableField import com.topjohnwu.magisk.utils.SuConnector import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter import me.tatarka.bindingcollectionadapter2.ItemBinding @@ -36,7 +36,7 @@ class SuRequestViewModel( private val policyDB: PolicyDao, private val timeoutPrefs: SharedPreferences, private val resources: Resources -) : MagiskViewModel() { +) : BaseViewModel() { val icon = KObservableField(null) val title = KObservableField("") 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 d08a20702..255d4965d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt @@ -21,9 +21,9 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewpager.widget.ViewPager import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.navigation.NavigationView -import com.skoumal.teanity.extensions.subscribeK import com.topjohnwu.magisk.R 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/DiffObservableList.kt b/app/src/main/java/com/topjohnwu/magisk/utils/DiffObservableList.kt new file mode 100644 index 000000000..c7c61d968 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/DiffObservableList.kt @@ -0,0 +1,234 @@ +package com.topjohnwu.magisk.utils + +import androidx.annotation.MainThread +import androidx.databinding.ListChangeRegistry +import androidx.databinding.ObservableList +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListUpdateCallback +import java.util.* +import kotlin.collections.ArrayList + +/** + * @param callback The callback that controls the behavior of the DiffObservableList. + * @param detectMoves True if DiffUtil should try to detect moved items, false otherwise. + */ +open class DiffObservableList( + private val callback: Callback, + private val detectMoves: Boolean = true +) : AbstractList(), ObservableList { + + private val LIST_LOCK = Object() + private var list: MutableList = ArrayList() + private val listeners = ListChangeRegistry() + private val listCallback = ObservableListUpdateCallback() + + override val size: Int get() = list.size + + /** + * Calculates the list of update operations that can convert this list into the given one. + * + * @param newItems The items that this list will be set to. + * @return A DiffResult that contains the information about the edit sequence to covert this + * list into the given one. + */ + fun calculateDiff(newItems: List): DiffUtil.DiffResult { + val frozenList = synchronized(LIST_LOCK) { + ArrayList(list) + } + return doCalculateDiff(frozenList, newItems) + } + + private fun doCalculateDiff(oldItems: List, newItems: List?): DiffUtil.DiffResult { + return DiffUtil.calculateDiff(object : DiffUtil.Callback() { + override fun getOldListSize() = oldItems.size + + override fun getNewListSize() = newItems?.size ?: 0 + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldItems[oldItemPosition] + val newItem = newItems!![newItemPosition] + return callback.areItemsTheSame(oldItem, newItem) + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldItems[oldItemPosition] + val newItem = newItems!![newItemPosition] + return callback.areContentsTheSame(oldItem, newItem) + } + }, detectMoves) + } + + /** + * Updates the contents of this list to the given one using the DiffResults to dispatch change + * notifications. + * + * @param newItems The items to set this list to. + * @param diffResult The diff results to dispatch change notifications. + */ + @MainThread + fun update(newItems: List, diffResult: DiffUtil.DiffResult) { + synchronized(LIST_LOCK) { + list = newItems.toMutableList() + } + diffResult.dispatchUpdatesTo(listCallback) + } + + /** + * Sets this list to the given items. This is a convenience method for calling [ ][.calculateDiff] followed by [.update]. + * + * + * **Warning!** If the lists are large this operation may be too slow for the main thread. In + * that case, you should call [.calculateDiff] on a background thread and then + * [.update] on the main thread. + * + * @param newItems The items to set this list to. + */ + @MainThread + fun update(newItems: List) { + val diffResult = doCalculateDiff(list, newItems) + list = newItems.toMutableList() + diffResult.dispatchUpdatesTo(listCallback) + } + + override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback>) { + listeners.add(listener) + } + + override fun removeOnListChangedCallback(listener: ObservableList.OnListChangedCallback>) { + listeners.remove(listener) + } + + override fun get(index: Int): T { + return list[index] + } + + override fun add(element: T): Boolean { + list.add(element) + notifyAdd(size - 1, 1) + return true + } + + override fun add(index: Int, element: T) { + list.add(index, element) + notifyAdd(index, 1) + } + + override fun addAll(elements: Collection): Boolean { + val oldSize = size + val added = list.addAll(elements) + if (added) { + notifyAdd(oldSize, size - oldSize) + } + return added + } + + override fun addAll(index: Int, elements: Collection): Boolean { + val added = list.addAll(index, elements) + if (added) { + notifyAdd(index, elements.size) + } + return added + } + + override fun clear() { + val oldSize = size + list.clear() + if (oldSize != 0) { + notifyRemove(0, oldSize) + } + } + + override fun remove(element: T): Boolean { + val index = indexOf(element) + return if (index >= 0) { + removeAt(index) + true + } else { + false + } + } + + override fun removeAt(index: Int): T { + val element = list.removeAt(index) + notifyRemove(index, 1) + return element + } + + fun removeLast(): T? { + if (size > 0) { + val index = size - 1 + return removeAt(index) + } + return null + } + + override fun set(index: Int, element: T): T { + val old = list.set(index, element) + listeners.notifyChanged(this, index, 1) + return old + } + + private fun notifyAdd(start: Int, count: Int) { + listeners.notifyInserted(this, start, count) + } + + private fun notifyRemove(start: Int, count: Int) { + listeners.notifyRemoved(this, start, count) + } + + /** + * A Callback class used by DiffUtil while calculating the diff between two lists. + */ + interface Callback { + /** + * Called by the DiffUtil to decide whether two object represent the same Item. + * + * + * For example, if your items have unique ids, this method should check their id equality. + * + * @param oldItem The old item. + * @param newItem The new item. + * @return True if the two items represent the same object or false if they are different. + */ + fun areItemsTheSame(oldItem: T, newItem: T): Boolean + + /** + * Called by the DiffUtil when it wants to check whether two items have the same data. + * DiffUtil uses this information to detect if the contents of an item has changed. + * + * + * DiffUtil uses this method to check equality instead of [Object.equals] so + * that you can change its behavior depending on your UI. + * + * + * This method is called only if [.areItemsTheSame] returns `true` for + * these items. + * + * @param oldItem The old item. + * @param newItem The new item which replaces the old item. + * @return True if the contents of the items are the same or false if they are different. + */ + fun areContentsTheSame(oldItem: T, newItem: T): Boolean + } + + inner class ObservableListUpdateCallback : ListUpdateCallback { + override fun onChanged(position: Int, count: Int, payload: Any?) { + listeners.notifyChanged(this@DiffObservableList, position, count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + listeners.notifyMoved(this@DiffObservableList, fromPosition, toPosition, 1) + } + + override fun onInserted(position: Int, count: Int) { + modCount += 1 + listeners.notifyInserted(this@DiffObservableList, position, count) + } + + override fun onRemoved(position: Int, count: Int) { + modCount += 1 + listeners.notifyRemoved(this@DiffObservableList, position, count) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/DynamicClassLoader.kt b/app/src/main/java/com/topjohnwu/magisk/utils/DynamicClassLoader.kt deleted file mode 100644 index 9810c91bd..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/utils/DynamicClassLoader.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.topjohnwu.magisk.utils - -import dalvik.system.DexClassLoader -import java.io.File -import java.io.IOException -import java.net.URL -import java.util.* - -@Suppress("FunctionName") -inline fun T.DynamicClassLoader(apk: File) = DynamicClassLoader(apk, T::class.java.classLoader) - -class DynamicClassLoader(apk: File, parent: ClassLoader?) - : DexClassLoader(apk.path, apk.parent, null, parent) { - - private val base by lazy { Any::class.java.classLoader!! } - - @Throws(ClassNotFoundException::class) - override fun loadClass(name: String, resolve: Boolean) : Class<*> - = findLoadedClass(name) ?: runCatching { - base.loadClass(name) - }.getOrElse { - runCatching { - findClass(name) - }.getOrElse { err -> - runCatching { - parent.loadClass(name) - }.getOrElse { throw err } - } - } - - override fun getResource(name: String) = base.getResource(name) - ?: findResource(name) - ?: parent?.getResource(name) - - @Throws(IOException::class) - override fun getResources(name: String): Enumeration { - val resources = mutableListOf( - base.getResources(name), - findResources(name), parent.getResources(name)) - return object : Enumeration { - override fun hasMoreElements(): Boolean { - while (true) { - if (resources.isEmpty()) - return false - if (!resources[0].hasMoreElements()) { - resources.removeAt(0) - } else { - return true - } - } - } - - override fun nextElement(): URL { - if (!hasMoreElements()) - throw NoSuchElementException() - return resources[0].nextElement() - } - } - } - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/KItemDecoration.kt b/app/src/main/java/com/topjohnwu/magisk/utils/KItemDecoration.kt new file mode 100644 index 000000000..0fc5c4f5c --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/KItemDecoration.kt @@ -0,0 +1,117 @@ +package com.topjohnwu.magisk.utils + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.View +import androidx.annotation.DrawableRes +import androidx.core.view.get +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.RecyclerView +import com.topjohnwu.magisk.extensions.drawableCompat + +class KItemDecoration( + private val context: Context, + @RecyclerView.Orientation private val orientation: Int +) : + RecyclerView.ItemDecoration() { + + private val bounds = Rect() + private var divider: Drawable? = null + var showAfterLast = true + + fun setDeco(@DrawableRes drawable: Int) = apply { + setDeco(context.drawableCompat(drawable)) + } + + fun setDeco(drawable: Drawable?) = apply { + divider = drawable + } + + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + parent.layoutManager ?: return + + divider?.let { + if (orientation == DividerItemDecoration.VERTICAL) { + drawVertical(canvas, parent, it) + } else { + drawHorizontal(canvas, parent, it) + } + } + } + + private fun drawVertical(canvas: Canvas, parent: RecyclerView, drawable: Drawable) { + canvas.save() + val left: Int + val right: Int + if (parent.clipToPadding) { + left = parent.paddingLeft + right = parent.width - parent.paddingRight + canvas.clipRect( + left, parent.paddingTop, right, + parent.height - parent.paddingBottom + ) + } else { + left = 0 + right = parent.width + } + + val to = if (showAfterLast) parent.childCount else parent.childCount - 1 + + (0 until to) + .map { parent[it] } + .forEach { child -> + parent.getDecoratedBoundsWithMargins(child, bounds) + val bottom = bounds.bottom + Math.round(child.translationY) + val top = bottom - drawable.intrinsicHeight + drawable.setBounds(left, top, right, bottom) + drawable.draw(canvas) + } + canvas.restore() + } + + private fun drawHorizontal(canvas: Canvas, parent: RecyclerView, drawable: Drawable) { + canvas.save() + val top: Int + val bottom: Int + if (parent.clipToPadding) { + top = parent.paddingTop + bottom = parent.height - parent.paddingBottom + canvas.clipRect( + parent.paddingLeft, top, + parent.width - parent.paddingRight, bottom + ) + } else { + top = 0 + bottom = parent.height + } + + val to = if (showAfterLast) parent.childCount else parent.childCount - 1 + + (0 until to) + .map { parent[it] } + .forEach { child -> + parent.layoutManager!!.getDecoratedBoundsWithMargins(child, bounds) + val right = bounds.right + Math.round(child.translationX) + val left = right - drawable.intrinsicWidth + drawable.setBounds(left, top, right, bottom) + drawable.draw(canvas) + } + canvas.restore() + } + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + if (parent.getChildAdapterPosition(view) == state.itemCount - 1) { + outRect.setEmpty() + return + } + + if (orientation == RecyclerView.VERTICAL) { + outRect.set(0, 0, 0, divider?.intrinsicHeight ?: 0) + } else { + outRect.set(0, 0, divider?.intrinsicWidth ?: 0, 0) + } + } + +} \ 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 new file mode 100644 index 000000000..7ecf125f4 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/KObservableField.kt @@ -0,0 +1,49 @@ +package com.topjohnwu.magisk.utils + +import androidx.databinding.Observable +import androidx.databinding.ObservableField +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 + */ +class KObservableField : ObservableField, Serializable { + + var value: T + set(value) { + if (field != value) { + field = value + notifyChange() + } + } + + constructor(init: T) { + value = init + } + + constructor(init: T, vararg dependencies: Observable) : super(*dependencies) { + value = init + } + + @Deprecated( + message = "Needed for data binding, use KObservableField.value syntax from code", + replaceWith = ReplaceWith("value") + ) + override fun get(): 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)" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/LocaleManager.kt b/app/src/main/java/com/topjohnwu/magisk/utils/LocaleManager.kt deleted file mode 100644 index e89241b0d..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/utils/LocaleManager.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.topjohnwu.magisk.utils - -import android.content.Context -import android.content.ContextWrapper -import android.content.res.Configuration -import android.content.res.Resources -import androidx.annotation.StringRes -import com.topjohnwu.magisk.Config -import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.extensions.get -import com.topjohnwu.magisk.extensions.inject -import com.topjohnwu.magisk.extensions.langTagToLocale -import com.topjohnwu.superuser.internal.InternalUtils -import io.reactivex.Single -import java.util.* - -var currentLocale = Locale.getDefault()!! - private set - -val defaultLocale = Locale.getDefault()!! - -val availableLocales = Single.fromCallable { - val compareId = R.string.app_changelog - val res: Resources by inject() - mutableListOf().apply { - // Add default locale - add(Locale.ENGLISH) - - // Add some special locales - add(Locale.TAIWAN) - add(Locale("pt", "BR")) - - // Other locales - val otherLocales = res.assets.locales - .map { it.langTagToLocale() } - .distinctBy { LocaleManager.getString(it, compareId) } - - listOf("", "").toTypedArray() - - addAll(otherLocales) - }.sortedWith(Comparator { a, b -> - a.getDisplayName(a).toLowerCase(a) - .compareTo(b.getDisplayName(b).toLowerCase(b)) - }) -}.cache()!! - -object LocaleManager { - - fun setLocale(wrapper: ContextWrapper) { - val localeConfig = Config.locale - currentLocale = when { - localeConfig.isEmpty() -> defaultLocale - else -> localeConfig.langTagToLocale() - } - Locale.setDefault(currentLocale) - InternalUtils.replaceBaseContext(wrapper, getLocaleContext(wrapper, currentLocale)) - } - - fun getLocaleContext(context: Context, locale: Locale = currentLocale): Context { - val config = Configuration(context.resources.configuration) - config.setLocale(locale) - return context.createConfigurationContext(config) - } - - fun getString(locale: Locale, @StringRes id: Int): String { - return getLocaleContext(get(), locale).getString(id) - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.kt b/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.kt index 74996f23a..1ed022ead 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.kt @@ -3,8 +3,8 @@ package com.topjohnwu.magisk.utils import android.content.ComponentName import android.content.Context import android.widget.Toast -import com.skoumal.teanity.extensions.subscribeK import com.topjohnwu.magisk.* +import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.ui.SplashActivity import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.signing.JarMap @@ -102,7 +102,7 @@ object PatchAPK { Config.suManager = pkg Config.export() - RootUtils.rmAndLaunch(BuildConfig.APPLICATION_ID, + Utils.rmAndLaunch(BuildConfig.APPLICATION_ID, ComponentName(pkg, ClassMap.get>(SplashActivity::class.java).name)) return true diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/ResourceMgr.kt b/app/src/main/java/com/topjohnwu/magisk/utils/ResourceMgr.kt new file mode 100644 index 000000000..fb3d7d503 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/ResourceMgr.kt @@ -0,0 +1,126 @@ +@file:Suppress("DEPRECATION") + +package com.topjohnwu.magisk.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.content.ContextWrapper +import android.content.res.AssetManager +import android.content.res.Configuration +import android.content.res.Resources +import androidx.annotation.StringRes +import com.topjohnwu.magisk.Config +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.extensions.langTagToLocale +import io.reactivex.Single +import java.util.* + +var isRunningAsStub = false + +var currentLocale: Locale = Locale.getDefault() + private set + +@SuppressLint("ConstantLocale") +val defaultLocale: Locale = Locale.getDefault() + +val availableLocales = Single.fromCallable { + val compareId = R.string.app_changelog + mutableListOf().apply { + // Add default locale + add(Locale.ENGLISH) + + // Add some special locales + add(Locale.TAIWAN) + add(Locale("pt", "BR")) + + val config = Configuration() + val metrics = ResourceMgr.resource.displayMetrics + val res = Resources(ResourceMgr.resource.assets, metrics, config) + + // Other locales + val otherLocales = ResourceMgr.resource.assets.locales + .map { it.langTagToLocale() } + .distinctBy { + config.setLocale(it) + res.updateConfiguration(config, metrics) + res.getString(compareId) + } + + listOf("", "").toTypedArray() + + addAll(otherLocales) + }.sortedWith(Comparator { a, b -> + a.getDisplayName(a).toLowerCase(a) + .compareTo(b.getDisplayName(b).toLowerCase(b)) + }) +}.cache()!! + +private val addAssetPath by lazy { + AssetManager::class.java.getMethod("addAssetPath", String::class.java) +} + +fun AssetManager.addAssetPath(path: String) { + addAssetPath.invoke(this, path) +} + +fun Context.wrap(global: Boolean = true): Context + = if (!global) ResourceMgr.ResContext(this) else ResourceMgr.GlobalResContext(this) + +object ResourceMgr { + + lateinit var resource: Resources + private lateinit var resApk: String + + fun init(context: Context) { + resource = context.resources + if (isRunningAsStub) + resApk = DynAPK.current(context).path + } + + // Override locale and inject resources from dynamic APK + private fun Resources.patch(config: Configuration = Configuration(configuration)): Resources { + config.setLocale(currentLocale) + updateConfiguration(config, displayMetrics) + if (isRunningAsStub) + assets.addAssetPath(resApk) + return this + } + + fun reload(config: Configuration = Configuration(resource.configuration)) { + val localeConfig = Config.locale + currentLocale = when { + localeConfig.isEmpty() -> defaultLocale + else -> localeConfig.langTagToLocale() + } + Locale.setDefault(currentLocale) + resource.patch(config) + } + + fun getString(locale: Locale, @StringRes id: Int): String { + val config = Configuration() + config.setLocale(locale) + return Resources(resource.assets, resource.displayMetrics, config).getString(id) + } + + open class GlobalResContext(base: Context) : ContextWrapper(base) { + open val mRes: Resources get() = resource + private val loader by lazy { javaClass.classLoader!! } + + override fun getResources(): Resources { + return mRes + } + + override fun getClassLoader(): ClassLoader { + return loader + } + + override fun createConfigurationContext(config: Configuration): Context { + return ResContext(super.createConfigurationContext(config)) + } + } + + class ResContext(base: Context) : GlobalResContext(base) { + override val mRes by lazy { base.resources.patch() } + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/RootInit.kt b/app/src/main/java/com/topjohnwu/magisk/utils/RootInit.kt new file mode 100644 index 000000000..c4bbfde4e --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/RootInit.kt @@ -0,0 +1,40 @@ +package com.topjohnwu.magisk.utils + +import android.content.Context +import com.topjohnwu.magisk.Const +import com.topjohnwu.magisk.Info +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.extensions.rawResource +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.ShellUtils +import com.topjohnwu.superuser.io.SuFile + +class RootInit : Shell.Initializer() { + + override fun onInit(context: Context, shell: Shell): Boolean { + return init(context.wrap(), shell) + } + + fun init(context: Context, shell: Shell): Boolean { + val job = shell.newJob() + if (shell.isRoot) { + job.add(context.rawResource(R.raw.util_functions)) + .add(context.rawResource(R.raw.utils)) + Const.MAGISK_DISABLE_FILE = SuFile("/cache/.disable_magisk") + Info.loadMagiskInfo() + } else { + job.add(context.rawResource(R.raw.nonroot_utils)) + } + + job.add("mount_partitions", + "get_flags", + "run_migrations", + "export BOOTMODE=true") + .exec() + + Info.keepVerity = ShellUtils.fastCmd("echo \$KEEPVERITY").toBoolean() + Info.keepEnc = ShellUtils.fastCmd("echo \$KEEPFORCEENCRYPT").toBoolean() + Info.recovery = ShellUtils.fastCmd("echo \$RECOVERYMODE").toBoolean() + return true + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/RootUtils.kt b/app/src/main/java/com/topjohnwu/magisk/utils/RootUtils.kt deleted file mode 100644 index 11bd46695..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/utils/RootUtils.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.topjohnwu.magisk.utils - -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.net.Uri -import com.topjohnwu.magisk.Const -import com.topjohnwu.magisk.Info -import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.extensions.rawResource -import com.topjohnwu.magisk.extensions.toShellCmd -import com.topjohnwu.superuser.Shell -import com.topjohnwu.superuser.ShellUtils -import com.topjohnwu.superuser.io.SuFile -import java.util.* -import java.lang.reflect.Array as RArray - -fun Intent.toCommand(args: MutableList) { - if (action != null) { - args.add("-a") - args.add(action!!) - } - if (component != null) { - args.add("-n") - args.add(component!!.flattenToString()) - } - if (data != null) { - args.add("-d") - args.add(dataString!!) - } - if (categories != null) { - for (cat in categories) { - args.add("-c") - args.add(cat) - } - } - if (type != null) { - args.add("-t") - args.add(type!!) - } - val extras = extras - if (extras != null) { - loop@ for (key in extras.keySet()) { - val v = extras.get(key) ?: continue - var value: Any = v - val arg: String - when { - v is String -> arg = "--es" - v is Boolean -> arg = "--ez" - v is Int -> arg = "--ei" - v is Long -> arg = "--el" - v is Float -> arg = "--ef" - v is Uri -> arg = "--eu" - v is ComponentName -> { - arg = "--ecn" - value = v.flattenToString() - } - v is ArrayList<*> -> { - if (v.size <= 0) - /* Impossible to know the type due to type erasure */ - continue@loop - - arg = if (v[0] is Int) - "--eial" - else if (v[0] is Long) - "--elal" - else if (v[0] is Float) - "--efal" - else if (v[0] is String) - "--esal" - else - continue@loop /* Unsupported */ - - val sb = StringBuilder() - for (o in v) { - sb.append(o.toString().replace(",", "\\,")) - sb.append(',') - } - // Remove trailing comma - sb.deleteCharAt(sb.length - 1) - value = sb - } - v.javaClass.isArray -> { - arg = if (v is IntArray) - "--eia" - else if (v is LongArray) - "--ela" - else if (v is FloatArray) - "--efa" - else if (v is Array<*> && v.isArrayOf()) - "--esa" - else - continue@loop /* Unsupported */ - - val sb = StringBuilder() - val len = RArray.getLength(v) - for (i in 0 until len) { - sb.append(RArray.get(v, i)!!.toString().replace(",", "\\,")) - sb.append(',') - } - // Remove trailing comma - sb.deleteCharAt(sb.length - 1) - value = sb - } - else -> continue@loop - } /* Unsupported */ - - args.add(arg) - args.add(key) - args.add(value.toString()) - } - } - args.add("-f") - args.add(flags.toString()) -} - -fun startActivity(intent: Intent) { - if (intent.component == null) - return - val args = ArrayList() - args.add("am") - args.add("start") - intent.toCommand(args) - Shell.su(args.toShellCmd()).exec() -} - -class RootUtils : Shell.Initializer() { - - override fun onInit(context: Context, shell: Shell): Boolean { - val job = shell.newJob() - if (shell.isRoot) { - job.add(context.rawResource(R.raw.util_functions)) - .add(context.rawResource(R.raw.utils)) - Const.MAGISK_DISABLE_FILE = SuFile("/cache/.disable_magisk") - Info.loadMagiskInfo() - } else { - job.add(context.rawResource(R.raw.nonroot_utils)) - } - - job.add("mount_partitions", - "get_flags", - "run_migrations", - "export BOOTMODE=true") - .exec() - - Info.keepVerity = ShellUtils.fastCmd("echo \$KEEPVERITY").toBoolean() - Info.keepEnc = ShellUtils.fastCmd("echo \$KEEPFORCEENCRYPT").toBoolean() - Info.recovery = ShellUtils.fastCmd("echo \$RECOVERYMODE").toBoolean() - return true - } - - companion object { - - fun rmAndLaunch(rm: String, component: ComponentName) { - Shell.su("(rm_launch $rm ${component.flattenToString()})").exec() - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/RxBus.kt b/app/src/main/java/com/topjohnwu/magisk/utils/RxBus.kt new file mode 100644 index 000000000..c27d898b8 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/RxBus.kt @@ -0,0 +1,36 @@ +package com.topjohnwu.magisk.utils + +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject + +class RxBus { + + private val _bus = PublishSubject.create() + + val bus: Observable get() = _bus + + fun post(event: Event) { + _bus.onNext(event) + } + + fun post(event: Int) { + _bus.onNext(SimpleEvent(event)) + } + + inline fun register(noinline predicate: (T) -> Boolean = { true }): Observable { + return bus + .ofType(T::class.java) + .filter(predicate) + } + + fun register(eventId: Int): Observable { + return bus + .ofType(SimpleEvent::class.java) + .map { it.eventId } + .filter { it == eventId } + } + + interface Event + + private class SimpleEvent(val eventId: Int) : Event +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/Utils.kt b/app/src/main/java/com/topjohnwu/magisk/utils/Utils.kt index bf3758fe6..4e873aa8a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/Utils.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/Utils.kt @@ -1,5 +1,6 @@ package com.topjohnwu.magisk.utils +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.res.Resources @@ -72,4 +73,8 @@ object Utils { if ((exists() && isDirectory) || mkdirs()) this else null } + fun rmAndLaunch(rm: String, component: ComponentName) { + Shell.su("(rm_launch $rm ${component.flattenToString()})").exec() + } + } 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 4a814104a..071913a63 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt +++ b/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt @@ -4,23 +4,22 @@ import android.content.Context import android.view.LayoutInflater import android.widget.TextView import androidx.appcompat.app.AlertDialog -import com.skoumal.teanity.extensions.subscribeK import com.topjohnwu.magisk.R import com.topjohnwu.magisk.data.repository.StringRepository -import com.topjohnwu.magisk.extensions.inject +import com.topjohnwu.magisk.extensions.subscribeK +import io.noties.markwon.Markwon import io.reactivex.Completable import io.reactivex.Single -import ru.noties.markwon.Markwon -import ru.noties.markwon.html.HtmlPlugin -import ru.noties.markwon.image.ImagesPlugin -import ru.noties.markwon.image.svg.SvgPlugin +import org.koin.core.KoinComponent +import org.koin.core.inject import timber.log.Timber import java.io.InputStream import java.util.* -object MarkDownWindow { +object MarkDownWindow : KoinComponent { private val stringRepo: StringRepository by inject() + private val markwon: Markwon by inject() fun show(activity: Context, title: String?, url: String) { show(activity, title, stringRepo.getString(url)) @@ -35,25 +34,14 @@ object MarkDownWindow { } fun show(activity: Context, title: String?, content: Single) { - val markwon = Markwon.builder(activity) - .usePlugin(HtmlPlugin.create()) - .usePlugin(ImagesPlugin.create(activity)) - .usePlugin(SvgPlugin.create(activity.resources)) - .build() val mv = LayoutInflater.from(activity).inflate(R.layout.markdown_window, null) val tv = mv.findViewById(R.id.md_txt) content.map { - runCatching { - markwon.setMarkdown(tv, it) - }.onFailure { - Timber.e(it) - // Always wrap the actual exception as it could be ExceptionInInitializerError, - // which is a fatal error and RxJava will send it to the global handler and crash - throw MarkwonException(it) - } + markwon.setMarkdown(tv, it) }.ignoreElement().onErrorResumeNext { // Nothing we can actually do other than show error message + Timber.e(it) tv.setText(R.string.download_file_error) Completable.complete() }.subscribeK { @@ -64,6 +52,4 @@ object MarkDownWindow { .show() } } - - class MarkwonException(e: Throwable): Exception(e) } 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 index 35f790d23..8eb37107b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/EnvFixDialog.kt +++ b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/EnvFixDialog.kt @@ -8,9 +8,9 @@ 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.magisk.net.Networking import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.internal.UiThreadHandler 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 index f2be2764f..4d0e0c09a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/InstallMethodDialog.kt +++ b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/InstallMethodDialog.kt @@ -7,13 +7,13 @@ 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.ui.base.MagiskActivity import com.topjohnwu.magisk.utils.Utils -internal class InstallMethodDialog(activity: MagiskActivity<*, *>, options: List) : +internal class InstallMethodDialog(activity: BaseActivity<*, *>, options: List) : AlertDialog.Builder(activity) { init { @@ -28,11 +28,11 @@ internal class InstallMethodDialog(activity: MagiskActivity<*, *>, options: List } } - private fun flash(activity: MagiskActivity<*, *>) = DownloadService(activity) { + private fun flash(activity: BaseActivity<*, *>) = DownloadService(activity) { subject = DownloadSubject.Magisk(Configuration.Flash.Primary) } - private fun patchBoot(activity: MagiskActivity<*, *>) = activity.withExternalRW { + 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) @@ -49,7 +49,7 @@ internal class InstallMethodDialog(activity: MagiskActivity<*, *>, options: List } } - private fun downloadOnly(activity: MagiskActivity<*, *>) = activity.withExternalRW { + private fun downloadOnly(activity: BaseActivity<*, *>) = activity.withExternalRW { onSuccess { DownloadService(activity) { subject = DownloadSubject.Magisk(Configuration.Download) @@ -57,7 +57,7 @@ internal class InstallMethodDialog(activity: MagiskActivity<*, *>, options: List } } - private fun installInactiveSlot(activity: MagiskActivity<*, *>) { + private fun installInactiveSlot(activity: BaseActivity<*, *>) { CustomAlertDialog(activity) .setTitle(R.string.warning) .setMessage(R.string.install_inactive_slot_msg) 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 index 54059b3ab..2f83b6f98 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/MagiskInstallDialog.kt +++ b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/MagiskInstallDialog.kt @@ -3,14 +3,14 @@ package com.topjohnwu.magisk.view.dialogs import android.net.Uri import com.topjohnwu.magisk.Info import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.ui.base.MagiskActivity +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: MagiskActivity<*, *>) : CustomAlertDialog(a) { +class MagiskInstallDialog(a: BaseActivity<*, *>) : CustomAlertDialog(a) { init { val filename = "Magisk v${Info.remote.magisk.version}" + "(${Info.remote.magisk.versionCode})" diff --git a/app/src/main/res/layout/fragment_magisk.xml b/app/src/main/res/layout/fragment_magisk.xml index e72cecda2..399aa75a2 100644 --- a/app/src/main/res/layout/fragment_magisk.xml +++ b/app/src/main/res/layout/fragment_magisk.xml @@ -5,7 +5,7 @@ - + diff --git a/app/src/main/res/raw/changelog.md b/app/src/main/res/raw/changelog.md index 367db5bf5..dd0d950b3 100644 --- a/app/src/main/res/raw/changelog.md +++ b/app/src/main/res/raw/changelog.md @@ -1,4 +1,4 @@ -# v7.3.4 -- App is now fully written in Kotlin! -- New downloading system -- Add new "Recovery Mode" to Advanced Settings +# v7.3.5 +- Sort installed modules by name +- Better pre-5.0 support +- Fix potential issues when patching tar files diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 0910dc4d7..708b2223a 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1,174 +1,226 @@ - إعدادات متقدمة - تغييرات التطبيق - انشئ بواسطة %1$s - استجابة تلقائية - البحث عن تحديثات… - التحقق من حالة SafetyNet… - إغلاق - الأمر: %1$s - رفض - سيتم تعطيل الإضافة في إعادة التشغيل التالي - سيتم تمكين الإضافة في إعادة التشغيل التالي - التنزيل - التنزيلات - للابد - سماح - التثبيت - مثبت - إبقاء AVB 2.0/dm-verity - الحفاظ علي قوه التشفير - السجل - تم حذف السجل بنجاح - تحديث Magisk جديد متوفر! - Magisk غير مثبت - حذف السجل الآن - إعادة تحميل - الإضافات - وضع تعدد المستخدمين - لا توجد تطبيقات - (لم يتم توفير أي معلومات) - لم يعثر على الإضافات - بدون - غير مثبت - مرة - يمكن للمالك فقط إدارة صلاحيات الروت وتلقي مطالبات الطلب - المالك فقط لديه صلاحيات الروت - طلب - إعادة التشغيل - ملاحظات الإصدار - سيتم حذف الإضافة في إعادة التشغيل التالي - لن يتم حذف الإضافة في إعادة التشغيل التالي - تم مسح الذاكرة المؤقته للمستودع - هل تريد تثبيت %1$s ? - تثبيت %1$s - مهلة الطلب - %1$d ثانية - نجح فحص SafetyNet - انقر لبدء فحص SafetyNet - الاستجابة غير صالحه - الإعدادات - حذف المعلومات المخزنة مؤقتا للمستودع على الانترنت، يجبر التطبيق لتحديث عبر الانترنت - حذف الذاكرة المؤقتة للمستودع - تمكين الميزات الأساسية فقط، لن يتم تحميل جميع الإضافات. MagiskSU، MagiskHide، systemless hosts، و لا يزال ممكنا - Magisk الوضع الأساسي فقط - تفعيل السمة الغامقة - السمة الغامقة - عام - Systemless يدعم تطبيقات حجب الإعلانات - تمكين المضيفين(الهوست) لـ systemless - إخفاء Magisk من مختلف الاكتشافات - إدارة مالك الجهاز - مالك الجهاز فقط - إعادة التشغيل لتطبيق الإعدادات - ADB فقط - التطبيقات فقط - التطبيقات و ADB - معطل - 10 ثواني - 20 ثانية - 30 ثانية - 60 ثانية - مستخدم مستقل - 60 دقائق - %1$s يتم منح صلاحيات Superuser - %1$s يتم رفض صلاحيات Superuser - Superuser طلبات - تأكيد لسحب صلاحيات %1$s ? - سحب؟ - Superuser الصلاحيات لـ %1$s تم رفضها - Superuser الصلاحيات لـ %1$s تم منحها - السجلات لـ %1$s تم تعطيلها - السجلات لـ %1$s تم تفعيلها - الإشعارات لـ %1$s تم تعطيلها - الإشعارات لـ %1$s تم تفعيلها - "منح حق الوصول الكامل إلى جهازك. -رفض إذا كنت غير متأكد!" - Superuser - Superuser صلاحيات - Superuser إشعارات - الهدف UID: %1$d - 10 دقائق - 30 دقائق - ملاحظة منبثقة - 20 دقائق - إلغاء التثبيت - إلغاء تثبيت Magisk - يتوفر تحديث - سيتم تحديث الإضافة في إعادة التشغيل التالي - كل مستخدم لديه قواعد روت منفصلة خاصة به - لا يدعم إصدار الأندرويد +8.0 - فشل المصادقة - مصادقة البصمة - إلغاء التثبيت بالكامل - تثبيت مباشر (موصى به) - لم يتم تعيين بصمات الأصابع أو لا يوجد جهاز مدعوم - تحميل ملف zip فقط - قام مدير Magisk بتصحيح dtbo.img ، يرجى إعادة التشغيل - تم تصحيح DTBO! - يحتاج جهازك إلى إعداد إضافي لـ Magisk للعمل بشكل صحيح. سيتم تنزيل ملف zip لتثبيت Magisk ، هل تريد المتابعة الآن؟ - يتطلب إعداد إضافي - التثبيت - تستخدم كافة جلسات الجذر مساحة الأسم ذات التركيب العالمي - فشل إخفاء مدير Magisk … - إخفاء مدير Magisk… - التثبيت على فتحة غير نشطة (بعد OTA) - "سيتم إجبار جهازك للتمهيد على الفتحة غير النشطة الحالية بعد إعادة التشغيل! -فقط استخدام هذا الخيار بعد الانتهاء من OTA. -استمرار؟" - قناة تحديث غير صالحة - سيكون لكل جلسة جذر مساحة الاسم الخاصة بها معزولة - اللغة - تحديثات Magisk - اضغط للتنزيل والتثبيت - تحديث مدير Magisk الجديد متوفر! - حفظ السجل - وضع تركيب مساحة الأسم - لم يتم العثور على تطبيق لفتح الرابط … - مدير Magisk هو FOSS ، والذي لا يحتوي على شفرة API الخاصة بشركة SafetyNet الخاصة بشركة Google. -هل تسمح لـ Magisk Manager بتنزيل ملحق (يحتوي على GoogleApiClient) لعمليات التحقق من SafetyNet؟ " - تحميل رمز الملكية - إعادة تمهيد إلى وضع Bootloader - إعادة تمهيد إلى وضع التحميل - إعادة تمهيد إلى وضع الإسترداد - "سترث جلسات الجذر مساحة الأسماء المطلوبة الخاصة بها" - تمت الأستعادة! - النسخ الاحتياطي الأصلي غير موجود! - استعادة الصور - الأستعادة … - خطأ SafetyNet API - حدد الطريقة - التحقق من التحديثات في الخلفية بشكل دوري - تفقد التحديث - أعد حزم مدير Magisk مع اسم حزمة عشوائية - إخفاء مدير Magisk - مساحة الاسم العالمية - مساحة الاسم المعزولة - وراثة مساحة الاسم - استعادة مدير Magisk مع الحزمة الأصلية - استعادة مدير Magisk - أستخدام ماسح بصمات الأصابع للسماح بطلبات المستخدم المتميز - تمكين مصادقة البصمة - أعد المصادقة على صلاحيات المستخدم المتميز بعد إجراء ترقيات للتطبيق - إعادة المصادقة بعد الترقية - تحديث الاعدادات - بيتا - قناة التحديث - مخصص - أدخل عنوان URL مخصص - مستقر - فشل الإعداد - تشغيل إعداد البيئة… - إعداد إضافي - الترتيب حسب الاسم - فرز حسب آخر تحديث - ترتيب الفرز - (أفتراضي النظام) - سيتم تعطيل/إزالة جميع الوحدات. ستتم إزالة الجذر ، وربما تشفير بياناتك إذا كانت بياناتك غير مشفرة حالياً - تحديث - تم التحديث في: %1$s - تحذير + + الإضافات + التنزيلات + المستخدم المتميز + السجل + الإعدادات + التثبيت + إصدار Magisk غير مدعوم + لا يدعم هذا الإصدار من Magisk Manager إصدارا لـ Magisk vأقل من 18\n\n بإمكانك تحديث Magisk يدويا أو تثبيت إصدار أدنى. + + + Magisk غير مثبت + البحث عن تحديثات… + قناة تحديث غير صالحة + انقر لبدء فحص SafetyNet + التحقق من حالة SafetyNet… + نجح فحص SafetyNet + خطأ SafetyNet API + رد غير صالح + Magisk محدث + Magisk Manager محدث + إعدادات متقدمة + إبقاء التشفير بقوة + إبقاء AVB 2.0/dm-verity + نمط الاستعادة + التحديث المثبت : %1$s + آخر تحديث : %1$s + إلغاء التثبيت + إلغاء تثبيت Magisk + ستُعطل/ستُحذف جميع الإضافات. سيُحذف الروت ، وربما ستشفر بياناتك إذا لم تكن غير مشفرة حالياً. + تحديث + (النمط الأساسي فقط ممكن) + + + (لم يتم توفير أي معلومات) + لم يعثر على أي إضافات + ستُحدث الإضافة في إعادة التشغيل التالي + ستُحذف الإضافة في إعادة التشغيل التالي + لن تُحذف الإضافة في إعادة التشغيل التالي + ستُعطل الإضافة في إعادة التشغيل التالي + ستُمكن الإضافة في إعادة التشغيل التالي + انشئ من طرف %1$s + إعادة التشغيل إلى نمط الاستعادة + إعادة التشغيل إلى نمط Bootloader + إعادة التشغيل إلى نمط التحميل + إعادة التشغيل إلى نمط EDL + + + يتوفر على تحديث + مثبت + غير مثبت + حُدث في: %1$s + ترتيب الفرز + افرز حسب الاسم + افرز حسب آخر تحديث + + + حفظ السجل + إعادة التحميل + حذف السجل الآن + حُذف السجل بنجاح + + + تغييرات التطبيق + + + تحديثات Magisk + إشعارات التقدم + التنزيل انتهى خطأ تنزيل الملف + أظهر والد المجلد + أظهر الملف + تحديث Magisk جديد متوفر! + تحديث Magisk Manager جديد متوفر! + + + اضغط للتنزيل و التثبيت + تحميل ملف Zip فقط + تثبيت مباشر (موصى بها) + التثبيت على فتحة غير نشطة (بعد OTA) + "سيُجبر جهازك للتشغيل على الفتحة غير النشطة الحالية بعد إعادة التشغيل! استخدم فقط هذا الخيار بعد الانتهاء من OTA. استمرار؟" + اختر الطريقة + إعداد إضافي + اختر و رقع ملفا + اختر ملفا خاما (*.img) أو ملف ODIN tarfile (*.tar) + إعادة التشغيل خلال خمس ثواني… + + + إغلاق + تثبيت %1$s + هل تريد تثبيت %1$s ? + التنزيل + إعادة التشغيل + إعادة التشغيل لتطبيق الإعدادات + ملاحظات الإصدار + مُسحت الذاكرة المؤقتة للإضافة + + رُقع DTBO! + رقع Magisk Manager الملف dtbo.img. يرجى إعادة التشغيل + …تثبيت + تم! + فشل + إخفاء Magisk Manager… + فشل إخفاء Magisk Manager… + لم يُعثر على تطبيق لفتح الرابط … + تحذير + إلغاء التثبيت بالكامل + استعادة الصور + استعادة… + استُعيدت! + النسخة الاحتياطية الأصلية غير موجودة! + تحميل شفرة المسجلة الملكية + لا Magisk Manager برنامج FOSS ،و لا يحتوي على شفرة SafetyNet API الخاصة بشركة Google.\n\nهل تسمح لـ Magisk Manager بتحميل ملحق (يحتوي على GoogleApiClient) للفحص SafetyNet؟ + فشل الإعداد + يتطلب إعدادا إضافيا + يحتاج جهازك إلى إعداد إضافي ليتسنى لـ Magisk بشكل صحيح. سينزل ملف Zip لتثبيت Magisk ، هل تريد المتابعة الآن؟ + تشغيل إعداد البيئة… + + + عام + السمة الغامقة + تمكين السمة الغامقة + مسار التحميل + ستحمل الملفات إلى %1$s + حذف الذاكرة المؤقتة للإضافات + حذف المعلومات المخزنة مؤقتا للإضافات المباشرة، هذا سيجبر التطبيق على التحديث عبر الانترنت + إخفاء Magisk Manager + أعد حزم Magisk Manager مع اسم حزمة عشوائي + استعادة Magisk Manager + استعادة Magisk Manager مع اسم الحزمة الأصلي + اللغة + (النظام المبدئي) + تحديث الإعدادات + تفقد التحديثات + التحقق من التحديثات في الخلفية بشكل دوري + قناة التحديث + مستقر + بيتا + مخصص + أدخل عنوانا URL مخصصا + النمط Magisk الأساسي فقط + تمكين الميزات الأساسية فقط، سيُشغل الإضافات MagiskSU و MagiskHide فقط و لا غيرها. + إخفاء Magisk من شتى طرق الاكتشاف + Systemless hosts + Systemless hosts يدعم تطبيقات حجب الإعلانات + مُكنت إضافة Systemless hosts + + التطبيقات و ADB + التطبيقات فقط + ADB فقط + معطل + 10 ثواني + 15 ثانية + 20 ثانية + 30 ثانية + 45 ثانية + 60 ثانية + صلاحيات المستخدم المتميز + رد تلقائي + مهلة الطلب + إشعارات المستخدم المتميز + %1$d ثانية + إعادة المصادقة بعد الترقية + أعد المصادقة على صلاحيات المستخدم المتميز بعد إجراء ترقيات للتطبيق + استخدام قارئ بصمات الأصابع للسماح بطلبات المستخدم المتميز + تمكين مصادقة البصمة + مصادقة البصمة + + نمط تعدد المستخدمين + مالك الجهاز فقط + إدارة مالك الجهاز + مستقل عن المستخدم + للمالك فقط صلاحيات الروت + يمكن للمالك فقط إدارة صلاحيات الروت وتلقي مطالبات الطلب + لكل مستخدم قواعد روت منفصلة خاصة به + + نمط تحميل مساحة الاسم + مساحة الاسم العامة + وراثة مساحة الاسم + مساحة الاسم المعزولة + تستخدم كافة جلسات العمل للروت مساحة الاسم ذات التركيب العامة + سترث جلسات العمل للروت مساحة الأسماء لطالبيها + سيكون لكل جلسة عمل للروت مساحة اسم معزولة خاصة بها + لا يدعم إصدار الأندرويد +8.0 + لم تُعين بصمات الأصابع أو لا يوجد قارئ بصمات + خطأ عند إنشاء مجلد. عليه أن يكون سهلا الوصول إليه من خلال مجلد التخزين للروت و ألا يكون ملفا. + + + طلبات المستخدم المتميز + رفض + طلب + سماح + "منح حق الوصول الكامل إلى جهازك. ارفض إذا كنت غير متأكد!" + للأبد + مرة + 10 دقائق + 20 دقائق + 30 دقائق + 60 دقائق + %1$s يتم منح صلاحيات + %1$s يتم رفض صلاحيات المستخدم المتميز + لا توجد تطبيقات + المستخدم المتميز الصلاحيات لـ %1$s تم منحها + المستخدم المتميز الصلاحيات لـ %1$s تم رفضها + الإشعارات لـ %1$s تم تفعيلها + الإشعارات لـ %1$s تم تعطيلها + السجلات لـ %1$s تم تفعيلها + السجلات لـ %1$s تم تعطيلها + سحب؟ + تأكيد لسحب صلاحيات %1$s ? + ملاحظة منبثقة + بدون + فشل المصادقة + + + PID: %1$d + Target UID: %1$d + Command: %1$s + + + اظهار برامج النظام + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c6478bf23..44722a4d6 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -24,6 +24,7 @@ Ajustes avanzados Mantener cifrado forzado Mantener AVB 2.0/dm-verity + Modo Recovery Instalada: %1$s Última: %1$s Desinstalar @@ -44,6 +45,7 @@ Reiniciar en Modo Recovery Reiniciar en Modo Bootloader Reiniciar en Modo Download + Reiniciar en Modo EDL Actualización Disponible @@ -77,7 +79,7 @@ Instalación Directa (Recomendado) Instalar en ranura inactiva (después de OTA) ¡Se forzará su dispositivo para que arranque en la ranura inactiva actual después de un reinicio!\nUtilice esta opción solo después de que se haya completado la OTA.\nContinuar? - Seleccionar Método + Seleccionar Método Configuración Adicional Seleccionar y parchear un archivo Seleccione una imagen raw (* .img) o un archivo tar de ODIN (* .tar) @@ -118,6 +120,8 @@ General Tema oscuro Habilitar el tema oscuro + Ruta de Descarga + Los archivos se guardarán en %1$s Limpiar caché del repositorio Limpiar la información en caché para los repositorios en línea, fuerza a la aplicación a actualizar en línea Ocultar Magisk Manager @@ -178,6 +182,7 @@ Cada sesión root tendrá su propia Namespace No es compatible con Android 8.0+ No se establecieron huellas dactilares o no existe soporte del dispositivo + Error al crear la carpeta. Debe ser accesible desde el directorio raíz de almacenamiento y no debe ser un archivo. Petición de superusuario diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index fd712283e..46006d63b 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -8,10 +8,10 @@ Seaded Installi Mittetoetatud Magisk\'i versioon - See Magisk Manager\'i versioon ei toeta Magisk\'i v18.0-st vanemat versiooni.\n\nSa võid kas Magisk\'i käsitsi täiendada või alandad rakenduse vanemale versioonile. + See Magisk Manager\'i versioon ei toeta Magisk\'ist vanemat versiooni kui v18.0.\n\nSa võid kas Magisk\'i käsitsi täiendada või alandad rakenduse vanemale versioonile. - Magisk pole installitud. + Magisk pole installitud Kontrollin uuendusi… Sobimatu uuenduste kanal Koputa SafetyNet\'i kontrolliks @@ -24,6 +24,7 @@ Täpsemad seaded Säilita sunnitud krüpteering Säilita AVB 2.0/dm-verity + Taastusrežiim Installitud: %1$s Viimatine: %1$s Eemalda @@ -44,6 +45,7 @@ Taaskäivita taastusesse Taaskäivita käivitushaldurisse Taaskäivita allalaadimisrežiimi + Taaskäivita EDL\'i Uuendus saadaval @@ -68,8 +70,22 @@ Edenemise teated Allalaadimine valmis Faili allalaadimisel esines viga + Kuva ülemkaustas + Kuva fail Magisk\'ile on uuendus saadaval! Magisk Manager\'ile on uuendus saadaval! + + + Vajuta allalaadimiseks ja installimiseks. + Laadi ainult ZIP alla + Otsene install (soovitatud) + Installi ebaaktiivsesse lahtrisse (pärast üle-õhu uuendust) + Sinu seade SUNNITAKSE peale taaskäivitust käivituma praegusesse ebaaktiivsesse lahtrisse!\nKasuta seda valikut vaid peale üle-õhu uuenduse teostamist.\nJätkad? + Vali meetod + Lisaseadistus + Vali ja paika fail + Vali toortõmmis (*.img) või ODIN tar-fail (*.tar) + Taaskäivitamine 5 sekundi pärast… Sulge @@ -80,19 +96,16 @@ Taaskäivita seadete rakendamiseks. Väljalaske märkmed Hoidla vahemälu tühjendatud - Vajuta allalaadimiseks ja installimiseks. + DTBO sai paigatud! Magisk Manager on paiganud dtbo.img. Palun taaskäivita. Välgutamine + Valmis! + Ebaõnnestus Peidan Magisk Manager\'i… Magisk Manager\'i peitmine ebaõnnestus. Lingi avamiseks sobivat rakendust ei leitud. - Laadi ainult ZIP alla - Otsene install (soovitatud) - Installi ebaaktiivsesse lahtrisse (pärast üle-õhu uuendust) Hoiatus - Sinu seade SUNNITAKSE peale taaskäivitust käivituma praegusesse ebaaktiivsesse lahtrisse!\nKasuta seda valikut vaid peale üle-õhu uuenduse teostamist.\nJätkad? - Vali meetod Täielik eemaldus Taasta tõmmised Taastamine… @@ -103,13 +116,14 @@ Seadistus ebaõnnnestus. Vajab lisaseadistust Sinu seade vajab lisaseadistust, et Magisk töötaks korralikult. Laadime alla Magisk\'i seadistus-zip\'i, kas soovid kohe jätkata? - Lisaseadistus Käivitan keskkonnaseadistust… Üldine Tume teema Luba tume teema. + Allalaadimise failitee + Failid salvestatakse kausta %1$s Tühjenda hoidla vahemälu Tühjenda vahemälus olev teave võrgus olevate hoidlate kohta. See sunnib rakendust võrgust värskendama. Peida Magisk Manager @@ -171,6 +185,7 @@ Iga juurkasutaja sessioon saab oma isoleeritud nimeruumi. Ei toeta Androidi versiooni 8.0+. Sõrmejälgi pole määratud või seade pole toetatud. + Faili loomisel esines viga. See peab olema ligipääsetav mäluruumi juurkaustast ning ei tohi olla fail. Superkasutaja taotlus diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 04dbb2e20..701a610a0 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -24,6 +24,7 @@ Gelişmiş Ayarlar Şifrelemeyi zorlamayı sürdür AVB 2.0/dm-verity\'i koru + Kurtarma Modu Yüklü: %1$s Güncel: %1$s Kaldır diff --git a/app/src/main/res/values-v19/styles.xml b/app/src/main/res/values-v19/styles.xml index 785a6291e..2a7f9b3e4 100644 --- a/app/src/main/res/values-v19/styles.xml +++ b/app/src/main/res/values-v19/styles.xml @@ -1,9 +1,4 @@ - - + - + + + + +