mirror of
https://github.com/topjohnwu/Magisk.git
synced 2024-11-27 20:15:29 +00:00
Merge remote-tracking branch 'john/master' into feature/redesign
# Conflicts: # app/build.gradle # app/src/main/AndroidManifest.xml # app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt # app/src/main/java/com/topjohnwu/magisk/model/navigation/MagiskNavigationEvent.kt # app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt # app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt # app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt # app/src/main/res/layout/dialog_magisk_base.xml
This commit is contained in:
commit
6ccbc272c6
@ -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 (`<module>/src/main/res/values-<lang>/strings.xml`).
|
||||
|
@ -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'
|
||||
}
|
||||
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@ -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 { *; }
|
||||
|
@ -1,4 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
** Special Requirements **
|
||||
|
||||
This AndroidManifest.xml will be copied into the stub
|
||||
APK to allow APK delegation. This is why these special
|
||||
requirements exist.
|
||||
|
||||
* Class names *
|
||||
Class names a.a, a.c, a.e should not be changed as they are used
|
||||
externally. All other class names can be changed.
|
||||
|
||||
* Resource IDs *
|
||||
All resource IDs referred in AndroidManifest.xml is required to be
|
||||
included into the "shared" module to make the ID match with stub.
|
||||
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.topjohnwu.magisk">
|
||||
@ -11,25 +29,17 @@
|
||||
|
||||
<application
|
||||
android:name="a.e"
|
||||
android:appComponentFactory="a.a"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/MagiskTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
|
||||
tools:replace="android:appComponentFactory">
|
||||
|
||||
<!-- Activities -->
|
||||
<!-- Splash -->
|
||||
|
||||
<activity
|
||||
android:name="a.b"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true" />
|
||||
<activity
|
||||
android:name="a.i"
|
||||
android:exported="true"
|
||||
android:theme="@style/Foundation.Default" />
|
||||
<activity
|
||||
android:name="a.c"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@ -37,6 +47,18 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Main -->
|
||||
|
||||
<activity
|
||||
android:name="a.b"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true" />
|
||||
|
||||
<activity
|
||||
android:name="a.i"
|
||||
android:exported="true"
|
||||
android:theme="@style/Foundation.Default" />
|
||||
|
||||
<activity-alias
|
||||
android:name="a.s"
|
||||
android:targetActivity="a.c">
|
||||
@ -46,11 +68,12 @@
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!-- Flashing -->
|
||||
|
||||
<activity
|
||||
android:name="a.f"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:screenOrientation="nosensor"
|
||||
android:theme="@style/MagiskTheme.Flashing" />
|
||||
android:screenOrientation="nosensor" />
|
||||
|
||||
<!-- Superuser -->
|
||||
|
||||
@ -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" />
|
||||
|
||||
<!-- Receiver -->
|
||||
|
||||
@ -78,9 +100,10 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Service -->
|
||||
<!-- DownloadService -->
|
||||
|
||||
<service android:name="a.j"
|
||||
<service
|
||||
android:name="a.j"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Hardcode GMS version -->
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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<T extends DelegateWorker> extends Worker {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
w(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
super(ResourceMgrKt.wrap(context, false), workerParams);
|
||||
try {
|
||||
base = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass())
|
||||
.getActualTypeArguments()[0]).newInstance();
|
||||
|
@ -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<ActivityTracker>())
|
||||
}
|
||||
|
||||
registerActivityLifecycleCallbacks(get<ActivityTracker>())
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
124
app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt
Normal file
124
app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt
Normal file
@ -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<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
|
||||
AppCompatActivity(), EventHandler {
|
||||
|
||||
protected lateinit var binding: Binding
|
||||
protected abstract val layoutRes: Int
|
||||
protected abstract val viewModel: ViewModel
|
||||
protected open val themeRes: Int = R.style.MagiskTheme
|
||||
protected open val snackbarView get() = binding.root
|
||||
|
||||
private val resultCallbacks by lazy { SparseArrayCompat<RequestCallback>() }
|
||||
|
||||
init {
|
||||
val theme = if (Config.darkTheme) {
|
||||
AppCompatDelegate.MODE_NIGHT_YES
|
||||
} else {
|
||||
AppCompatDelegate.MODE_NIGHT_NO
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(theme)
|
||||
}
|
||||
|
||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
||||
// Force applying our preferred local
|
||||
config?.setLocale(currentLocale)
|
||||
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<Binding>(this, layoutRes).apply {
|
||||
setVariable(BR.viewModel, viewModel)
|
||||
lifecycleOwner = this@BaseActivity
|
||||
}
|
||||
}
|
||||
|
||||
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
|
||||
val request = PermissionRequestBuilder().apply(builder).build()
|
||||
val ungranted = permissions.filter {
|
||||
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<out String>, 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)
|
||||
}
|
||||
|
||||
}
|
50
app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt
Normal file
50
app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt
Normal file
@ -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<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
|
||||
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<Binding>(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
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
17
app/src/main/java/com/topjohnwu/magisk/base/BaseReceiver.kt
Normal file
17
app/src/main/java/com/topjohnwu/magisk/base/BaseReceiver.kt
Normal file
@ -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?)
|
||||
}
|
12
app/src/main/java/com/topjohnwu/magisk/base/BaseService.kt
Normal file
12
app/src/main/java/com/topjohnwu/magisk/base/BaseService.kt
Normal file
@ -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())
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.model.worker
|
||||
package com.topjohnwu.magisk.base
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Network
|
@ -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())
|
@ -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<LoadingViewModel.State>(defaultState) {
|
||||
|
||||
val loading @Bindable get() = state == State.LOADING
|
||||
val loaded @Bindable get() = state == State.LOADED
|
||||
val loadingFailed @Bindable get() = state == State.LOADING_FAILED
|
||||
|
||||
@Deprecated(
|
||||
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
|
||||
ReplaceWith("state = State.LOADING", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
|
||||
DeprecationLevel.WARNING
|
||||
)
|
||||
fun setLoading() {
|
||||
state = State.LOADING
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
|
||||
ReplaceWith("state = State.LOADED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
|
||||
DeprecationLevel.WARNING
|
||||
)
|
||||
fun setLoaded() {
|
||||
state = State.LOADED
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
|
||||
ReplaceWith("state = State.LOADING_FAILED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
|
||||
DeprecationLevel.WARNING
|
||||
)
|
||||
fun setLoadingFailed() {
|
||||
state = State.LOADING_FAILED
|
||||
}
|
||||
|
||||
override fun notifyStateChanged() {
|
||||
notifyPropertyChanged(BR.loading)
|
||||
notifyPropertyChanged(BR.loaded)
|
||||
notifyPropertyChanged(BR.loadingFailed)
|
||||
}
|
||||
|
||||
enum class State {
|
||||
LOADED, LOADING, LOADING_FAILED
|
||||
}
|
||||
|
||||
//region Rx
|
||||
protected fun <T> Observable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnNext { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun <T> Single<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun <T> Maybe<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnComplete { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
.doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun <T> Flowable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnNext { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun Completable.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnComplete { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
//endregion
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
abstract class StatefulViewModel<State : Enum<*>>(
|
||||
val defaultState: State
|
||||
) : ObservableViewModel() {
|
||||
|
||||
var state: State = defaultState
|
||||
set(value) {
|
||||
field = value
|
||||
notifyStateChanged()
|
||||
}
|
||||
|
||||
open fun notifyStateChanged() = Unit
|
||||
|
||||
}
|
@ -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<ViewEvent>()
|
||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun <Event : ViewEvent> Event.publish() {
|
||||
_viewEvents.value = this
|
||||
}
|
||||
|
||||
fun Int.publish() {
|
||||
_viewEvents.value = SimpleViewEvent(this)
|
||||
}
|
||||
|
||||
fun Disposable.add() {
|
||||
disposables.add(this)
|
||||
}
|
||||
}
|
@ -19,10 +19,10 @@ interface GithubRawServices {
|
||||
@GET("$MAGISK_FILES/master/beta.json")
|
||||
fun fetchBetaUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/master/canary_builds/release.json")
|
||||
@GET("$MAGISK_FILES/canary/release.json")
|
||||
fun fetchCanaryUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/master/canary_builds/canary.json")
|
||||
@GET("$MAGISK_FILES/canary/debug.json")
|
||||
fun fetchCanaryDebugUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET
|
||||
|
@ -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())
|
||||
}
|
@ -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)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
|
||||
open class BindingBoundAdapter : BindingRecyclerViewAdapter<RvItem>() {
|
||||
|
||||
override fun onBindBinding(binding: ViewDataBinding, variableId: Int, layoutRes: Int, position: Int, item: RvItem) {
|
||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
||||
|
||||
item.onBindingBound(binding)
|
||||
}
|
||||
}
|
@ -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<in T> : 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<ComparableRvItem<*>> {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ComparableRvItem<*>,
|
||||
newItem: ComparableRvItem<*>
|
||||
) = oldItem.genericItemSameAs(newItem)
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ComparableRvItem<*>,
|
||||
newItem: ComparableRvItem<*>
|
||||
) = oldItem.genericContentSameAs(newItem)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
|
||||
single { createApiService<GithubApiServices>(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 <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseU
|
||||
.baseUrl(baseUrl)
|
||||
.build()
|
||||
.create(T::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
|
||||
return Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(ImagesPlugin.create {
|
||||
it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient))
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
@ -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 <T> ObservableField<T>.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 <T> ObservableField<T>.update(block: (T?) -> Unit) {
|
||||
set(get().apply(block))
|
||||
}
|
||||
|
||||
inline fun <T> ObservableField<T>.updateNonNull(block: (T) -> Unit) {
|
||||
update {
|
||||
it ?: return@update
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun ObservableInt.update(block: (Int) -> Unit) {
|
||||
set(get().apply(block))
|
||||
}
|
@ -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()
|
@ -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)
|
201
app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt
Normal file
201
app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt
Normal file
@ -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 <T> Observable<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Observable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Flowable<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Flowable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Single<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Single<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Maybe<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Maybe<T> = 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> = (T) -> Unit
|
||||
typealias OnErrorListener = (Throwable) -> Unit
|
||||
|
||||
/*=== ALIASES FOR OBSERVABLES ===*/
|
||||
|
||||
fun <T> Observable<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun <T> Single<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError)
|
||||
|
||||
fun <T> Maybe<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun <T> Flowable<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun Completable.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onComplete, onError)
|
||||
|
||||
|
||||
fun <T> Observable<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnNextUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
|
||||
fun <T> Single<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnSuccessUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
|
||||
fun <T> Maybe<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnSuccessUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
.doOnComplete { field.value = field.value }
|
||||
|
||||
fun <T> Flowable<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnNextUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
|
||||
fun Completable.updateBy(
|
||||
field: KObservableField<Boolean>
|
||||
) = doOnCompleteUi { field.value = true }
|
||||
.doOnErrorUi { field.value = false }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
fun <T> Single<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
fun <T> Flowable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
fun Completable.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
fun Completable.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { ui { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { ui { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { ui { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { ui { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { ui { body() } }
|
||||
|
||||
fun Completable.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { ui { body() } }
|
||||
|
||||
|
||||
fun <T, R> Observable<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flatMapIterable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Single<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flattenAsFlowable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Maybe<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flattenAsFlowable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Flowable<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flatMapIterable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T> ObservableField<T>.toObservable(): Observable<T> {
|
||||
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 : Any> T.toSingle() = Single.just(this)
|
||||
|
||||
fun <T1, T2, R> zip(t1: Single<T1>, t2: Single<T2>, zipper: (T1, T2) -> R) =
|
||||
Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })
|
126
app/src/main/java/com/topjohnwu/magisk/extensions/Snackbar.kt
Normal file
126
app/src/main/java/com/topjohnwu/magisk/extensions/Snackbar.kt
Normal file
@ -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<TextView>(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)
|
||||
}
|
@ -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<Context>().packageName
|
||||
|
||||
@ -85,6 +97,105 @@ fun Context.readUri(uri: Uri) =
|
||||
|
||||
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
||||
|
||||
fun Intent.toCommand(args: MutableList<String>) {
|
||||
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<String>())
|
||||
"--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<Int, Int> {
|
||||
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 <reified T> 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
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
|
||||
|
||||
fun KObservableField<Boolean>.toggle() {
|
||||
|
@ -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 <T> MutableList<T>.update(newList: List<T>) {
|
||||
@ -26,8 +25,8 @@ fun List<String>.toShellCmd(): String {
|
||||
}
|
||||
|
||||
fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
|
||||
target: DiffObservableList<T2>,
|
||||
mapper: (List<T1>) -> List<T2>
|
||||
target: DiffObservableList<T2>,
|
||||
mapper: (List<T1>) -> List<T2>
|
||||
) = addOnListChangedCallback(object :
|
||||
ObservableList.OnListChangedCallback<ObservableList<T1>>() {
|
||||
override fun onChanged(sender: ObservableList<T1>?) {
|
||||
|
@ -1,9 +0,0 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.BiFunction
|
||||
|
||||
fun <T : Any> T.toSingle() = Single.just(this)
|
||||
|
||||
fun <T1, T2, R> zip(t1: Single<T1>, t2: Single<T2>, zipper: (T1, T2) -> R) =
|
||||
Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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<Class<*>>(SplashActivity::class.java).name)
|
||||
RootUtils.rmAndLaunch(packageName, component)
|
||||
Utils.rmAndLaunch(packageName, component)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.*
|
||||
|
@ -1,3 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
data class Version(val version: String, val versionCode: Int)
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<HideTarget>) :
|
||||
ComparableRvItem<HideRvItem>() {
|
||||
|
@ -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
|
||||
|
@ -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<LogRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_page_log
|
||||
|
@ -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<ModuleRvItem>() {
|
||||
|
||||
|
@ -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<PolicyRvItem>() {
|
||||
|
||||
|
@ -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<SectionRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_section
|
||||
|
@ -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<SpinnerRvItem>() {
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
package com.topjohnwu.magisk.model.events
|
||||
|
||||
class SimpleViewEvent(
|
||||
val event: Int
|
||||
) : ViewEvent()
|
@ -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)
|
||||
}
|
@ -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<ViewEvent> {
|
||||
override fun onChanged(event: ViewEvent?) {
|
||||
event?.let {
|
||||
if (!it.handled) {
|
||||
it.handled = true
|
||||
onEventUnhandled(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
class DieEvent : ViewEvent()
|
||||
|
@ -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,
|
||||
|
@ -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 -> {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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<String>
|
||||
private val logs: MutableList<String>
|
||||
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")
|
||||
|
@ -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<MainViewModel, ActivityMainBinding>() {
|
||||
open class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>(), 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<KClass<out Fragment>> = listOf(
|
||||
HomeFragment::class,
|
||||
@ -43,10 +59,6 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
|
||||
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<MainViewModel, ActivityMainBinding>() {
|
||||
}
|
||||
|
||||
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<MainViewModel, ActivityMainBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
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<MainViewModel, ActivityMainBinding>() {
|
||||
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<MainViewModel, ActivityMainBinding>() {
|
||||
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
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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<View>(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)
|
||||
}
|
||||
}
|
@ -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<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
|
||||
TeanityActivity<ViewModel, Binding>(), FragNavController.RootFragmentListener,
|
||||
Navigator, FragNavController.TransactionListener {
|
||||
|
||||
override val numberOfRootFragments: Int get() = baseFragments.size
|
||||
override val baseFragments: List<KClass<out Fragment>> = listOf()
|
||||
private val resultCallbacks = SparseArrayCompat<RequestCallback>()
|
||||
|
||||
|
||||
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<PermissionRequest>,
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
@ -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<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
|
||||
TeanityFragment<ViewModel, Binding>(), Navigator {
|
||||
|
||||
protected val activity get() = requireActivity() as MagiskActivity<*, *>
|
||||
|
||||
// We don't need nested fragments
|
||||
override val baseFragments: List<KClass<Fragment>> = 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
|
||||
|
||||
}
|
@ -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<FlashViewModel, ActivityFlashBinding>() {
|
||||
open class FlashActivity : BaseActivity<FlashViewModel, ActivityFlashBinding>() {
|
||||
|
||||
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<FlashViewModel, ActivityFlashBinding>(
|
||||
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])
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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<HideViewModel, FragmentMagiskHideBinding>(),
|
||||
class MagiskHideFragment : BaseFragment<HideViewModel, FragmentMagiskHideBinding>(),
|
||||
SearchView.OnQueryTextListener {
|
||||
|
||||
override val layoutRes: Int = R.layout.fragment_magisk_hide
|
||||
|
@ -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<HomeViewModel, FragmentMagiskBinding>(),
|
||||
class HomeFragment : BaseFragment<HomeViewModel, FragmentMagiskBinding>(),
|
||||
SafetyNetHelper.Callback {
|
||||
|
||||
override val layoutRes: Int = R.layout.fragment_magisk
|
||||
@ -33,13 +33,14 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
|
||||
|
||||
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<HomeViewModel, FragmentMagiskBinding>(),
|
||||
return
|
||||
}
|
||||
|
||||
MagiskInstallDialog(requireActivity() as MagiskActivity<*, *>).show()
|
||||
MagiskInstallDialog(requireActivity() as BaseActivity<*, *>).show()
|
||||
}
|
||||
|
||||
private fun installManager() = ManagerInstallDialog(requireActivity()).show()
|
||||
@ -94,7 +95,7 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
|
||||
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
|
||||
|
@ -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<PackageManager>().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
|
||||
|
@ -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<LogViewModel, FragmentLogBinding>() {
|
||||
class LogFragment : BaseFragment<LogViewModel, FragmentLogBinding>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.fragment_log
|
||||
override val viewModel: LogViewModel by viewModel()
|
||||
|
@ -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<ComparableRvItem<*>> {
|
||||
) : BaseViewModel(), BindingViewPagerAdapter.PageTitles<ComparableRvItem<*>> {
|
||||
|
||||
val itemsAdapter = BindingAdapter()
|
||||
val items = DiffObservableList(ComparableRvItem.callback)
|
||||
|
@ -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("")
|
||||
|
||||
|
@ -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<ModuleViewModel, FragmentModulesBinding>() {
|
||||
class ModulesFragment : BaseFragment<ModuleViewModel, FragmentModulesBinding>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.fragment_modules
|
||||
override val viewModel: ModuleViewModel by sharedViewModel()
|
||||
|
@ -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<ModuleViewModel, FragmentReposBinding>(),
|
||||
class ReposFragment : BaseFragment<ModuleViewModel, FragmentReposBinding>(),
|
||||
SearchView.OnQueryTextListener {
|
||||
|
||||
override val layoutRes: Int = R.layout.fragment_repos
|
||||
|
@ -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<String>()
|
||||
|
||||
names.add(
|
||||
LocaleManager.getString(defaultLocale, R.string.system_default)
|
||||
ResourceMgr.getString(defaultLocale, R.string.system_default)
|
||||
)
|
||||
values.add("")
|
||||
|
||||
|
@ -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<SuperuserViewModel, FragmentSuperuserBinding>() {
|
||||
BaseFragment<SuperuserViewModel, FragmentSuperuserBinding>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.fragment_superuser
|
||||
override val viewModel: SuperuserViewModel by viewModel()
|
||||
|
@ -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<ComparableRvItem<*>> { itemBinding, _, item ->
|
||||
|
@ -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<SuRequestViewModel, ActivityRequestBinding>() {
|
||||
open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestBinding>() {
|
||||
|
||||
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() {
|
||||
|
@ -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<Drawable?>(null)
|
||||
val title = KObservableField("")
|
||||
|
@ -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
|
||||
|
@ -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<T>(
|
||||
private val callback: Callback<T>,
|
||||
private val detectMoves: Boolean = true
|
||||
) : AbstractList<T>(), ObservableList<T> {
|
||||
|
||||
private val LIST_LOCK = Object()
|
||||
private var list: MutableList<T> = 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<T>): DiffUtil.DiffResult {
|
||||
val frozenList = synchronized(LIST_LOCK) {
|
||||
ArrayList(list)
|
||||
}
|
||||
return doCalculateDiff(frozenList, newItems)
|
||||
}
|
||||
|
||||
private fun doCalculateDiff(oldItems: List<T>, newItems: List<T>?): 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<T>, 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<T>) {
|
||||
val diffResult = doCalculateDiff(list, newItems)
|
||||
list = newItems.toMutableList()
|
||||
diffResult.dispatchUpdatesTo(listCallback)
|
||||
}
|
||||
|
||||
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun removeOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
||||
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<T>): Boolean {
|
||||
val oldSize = size
|
||||
val added = list.addAll(elements)
|
||||
if (added) {
|
||||
notifyAdd(oldSize, size - oldSize)
|
||||
}
|
||||
return added
|
||||
}
|
||||
|
||||
override fun addAll(index: Int, elements: Collection<T>): 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<T> {
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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 <reified T> 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<URL> {
|
||||
val resources = mutableListOf(
|
||||
base.getResources(name),
|
||||
findResources(name), parent.getResources(name))
|
||||
return object : Enumeration<URL> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
117
app/src/main/java/com/topjohnwu/magisk/utils/KItemDecoration.kt
Normal file
117
app/src/main/java/com/topjohnwu/magisk/utils/KItemDecoration.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<T> : ObservableField<T>, 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)"
|
||||
}
|
||||
}
|
@ -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<Locale>().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)
|
||||
}
|
||||
}
|
@ -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<Class<*>>(SplashActivity::class.java).name))
|
||||
|
||||
return true
|
||||
|
126
app/src/main/java/com/topjohnwu/magisk/utils/ResourceMgr.kt
Normal file
126
app/src/main/java/com/topjohnwu/magisk/utils/ResourceMgr.kt
Normal file
@ -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<Locale>().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() }
|
||||
}
|
||||
|
||||
}
|
40
app/src/main/java/com/topjohnwu/magisk/utils/RootInit.kt
Normal file
40
app/src/main/java/com/topjohnwu/magisk/utils/RootInit.kt
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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<String>) {
|
||||
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<String>())
|
||||
"--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<String>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
36
app/src/main/java/com/topjohnwu/magisk/utils/RxBus.kt
Normal file
36
app/src/main/java/com/topjohnwu/magisk/utils/RxBus.kt
Normal file
@ -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<Event>()
|
||||
|
||||
val bus: Observable<Event> get() = _bus
|
||||
|
||||
fun post(event: Event) {
|
||||
_bus.onNext(event)
|
||||
}
|
||||
|
||||
fun post(event: Int) {
|
||||
_bus.onNext(SimpleEvent(event))
|
||||
}
|
||||
|
||||
inline fun <reified T : Event> register(noinline predicate: (T) -> Boolean = { true }): Observable<T> {
|
||||
return bus
|
||||
.ofType(T::class.java)
|
||||
.filter(predicate)
|
||||
}
|
||||
|
||||
fun register(eventId: Int): Observable<Int> {
|
||||
return bus
|
||||
.ofType(SimpleEvent::class.java)
|
||||
.map { it.eventId }
|
||||
.filter { it == eventId }
|
||||
}
|
||||
|
||||
interface Event
|
||||
|
||||
private class SimpleEvent(val eventId: Int) : Event
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<String>) {
|
||||
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<TextView>(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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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<String>) :
|
||||
internal class InstallMethodDialog(activity: BaseActivity<*, *>, options: List<String>) :
|
||||
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)
|
||||
|
@ -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})"
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.skoumal.teanity.viewmodel.LoadingViewModel.State" />
|
||||
<import type="com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State" />
|
||||
|
||||
<import type="com.topjohnwu.magisk.ui.home.SafetyNetState" />
|
||||
|
||||
|
@ -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
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user