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:
Viktor De Pasquale 2019-10-16 17:33:54 +02:00
commit 6ccbc272c6
208 changed files with 4215 additions and 1929 deletions

View File

@ -33,7 +33,6 @@ Furthermore, Magisk provides a **Systemless Interface** to alter the system (or
Default string resources for Magisk Manager are scattered throughout Default string resources for Magisk Manager are scattered throughout
- `app/src/main/res/values/strings.xml` - `app/src/main/res/values/strings.xml`
- `stub/src/main/res/values/strings.xml`
- `shared/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`). Translate each and place them in the respective locations (`<module>/src/main/res/values-<lang>/strings.xml`).

View File

@ -60,14 +60,26 @@ dependencies {
implementation 'com.github.topjohnwu:jtar:1.0.0' implementation 'com.github.topjohnwu:jtar:1.0.0'
implementation 'com.jakewharton.timber:timber:4.7.1' 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.ncapdevi:frag-nav:3.2.0'
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6' implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6'
def vMarkwon = '3.1.0' implementation 'io.reactivex.rxjava2:rxjava:2.2.13'
implementation "ru.noties.markwon:core:${vMarkwon}" implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
implementation "ru.noties.markwon:html:${vMarkwon}" implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation "ru.noties.markwon:image-svg:${vMarkwon}"
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' def vLibsu = '2.5.1'
implementation "com.github.topjohnwu.libsu:core:${vLibsu}" implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
@ -78,13 +90,13 @@ dependencies {
implementation "org.koin:koin-android:${vKoin}" implementation "org.koin:koin-android:${vKoin}"
implementation "org.koin:koin-androidx-viewmodel:${vKoin}" implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
def vRetrofit = '2.6.1' def vRetrofit = '2.6.2'
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}" implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}" implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}" implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}" implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
def vOkHttp = '3.12.2' def vOkHttp = '3.12.6'
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}" implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}" implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
@ -100,19 +112,21 @@ dependencies {
replacedBy('com.github.topjohnwu:room-runtime') replacedBy('com.github.topjohnwu:room-runtime')
} }
} }
def vRoom = "2.1.0" def vRoom = "2.2.0"
implementation "com.github.topjohnwu:room-runtime:${vRoom}" implementation "com.github.topjohnwu:room-runtime:${vRoom}"
kapt "androidx.room:room-compiler:${vRoom}" kapt "androidx.room:room-compiler:${vRoom}"
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVer}" def vNav = "2.1.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVer}" implementation "androidx.navigation:navigation-fragment-ktx:$vNav"
implementation "androidx.navigation:navigation-ui-ktx:$vNav"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 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.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.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.multidex:multidex:2.0.1'
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'com.google.android.material:material:1.1.0-beta01' implementation 'com.google.android.material:material:1.1.0-beta01'
} }

View File

@ -29,7 +29,7 @@
} }
# DelegateWorker # DelegateWorker
-keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker -keep,allowobfuscation class * extends com.topjohnwu.magisk.base.DelegateWorker
# BootSigner # BootSigner
-keepclassmembers class com.topjohnwu.signing.BootSigner { *; } -keepclassmembers class com.topjohnwu.signing.BootSigner { *; }

View File

@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?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" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.topjohnwu.magisk"> package="com.topjohnwu.magisk">
@ -11,25 +29,17 @@
<application <application
android:name="a.e" android:name="a.e"
android:appComponentFactory="a.a"
android:allowBackup="true" android:allowBackup="true"
android:theme="@style/MagiskTheme"
android:usesCleartextTraffic="true" 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 <activity
android:name="a.c" android:name="a.c"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:exported="true"
android:theme="@style/SplashTheme"> android:theme="@style/SplashTheme">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -37,6 +47,18 @@
</intent-filter> </intent-filter>
</activity> </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 <activity-alias
android:name="a.s" android:name="a.s"
android:targetActivity="a.c"> android:targetActivity="a.c">
@ -46,11 +68,12 @@
</intent-filter> </intent-filter>
</activity-alias> </activity-alias>
<!-- Flashing -->
<activity <activity
android:name="a.f" android:name="a.f"
android:configChanges="keyboardHidden|orientation|screenSize" android:configChanges="keyboardHidden|orientation|screenSize"
android:screenOrientation="nosensor" android:screenOrientation="nosensor" />
android:theme="@style/MagiskTheme.Flashing" />
<!-- Superuser --> <!-- Superuser -->
@ -58,8 +81,7 @@
android:name="a.m" android:name="a.m"
android:directBootAware="true" android:directBootAware="true"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="false" android:exported="false" />
android:theme="@style/MagiskTheme.SU" />
<!-- Receiver --> <!-- Receiver -->
@ -78,9 +100,10 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- Service --> <!-- DownloadService -->
<service android:name="a.j" <service
android:name="a.j"
android:exported="false" /> android:exported="false" />
<!-- Hardcode GMS version --> <!-- Hardcode GMS version -->

View File

@ -1,13 +1,19 @@
package a; package a;
import androidx.annotation.Keep;
import androidx.core.app.AppComponentFactory;
import com.topjohnwu.magisk.utils.PatchAPK; import com.topjohnwu.magisk.utils.PatchAPK;
import com.topjohnwu.signing.BootSigner; import com.topjohnwu.signing.BootSigner;
import androidx.annotation.Keep;
@Keep @Keep
public class a extends BootSigner { public class a extends AppComponentFactory {
public static boolean patchAPK(String in, String out, String pkg) { public static boolean patchAPK(String in, String out, String pkg) {
return PatchAPK.patch(in, out, pkg); return PatchAPK.patch(in, out, pkg);
} }
public static void main(String[] args) throws Exception {
BootSigner.main(args);
}
} }

View File

@ -6,7 +6,8 @@ import androidx.annotation.NonNull;
import androidx.work.Worker; import androidx.work.Worker;
import androidx.work.WorkerParameters; 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; import java.lang.reflect.ParameterizedType;
@ -18,7 +19,7 @@ public abstract class w<T extends DelegateWorker> extends Worker {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
w(@NonNull Context context, @NonNull WorkerParameters workerParams) { w(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams); super(ResourceMgrKt.wrap(context, false), workerParams);
try { try {
base = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()) base = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass())
.getActualTypeArguments()[0]).newInstance(); .getActualTypeArguments()[0]).newInstance();

View File

@ -13,9 +13,11 @@ import com.topjohnwu.magisk.data.database.RepoDatabase_Impl
import com.topjohnwu.magisk.di.ActivityTracker import com.topjohnwu.magisk.di.ActivityTracker
import com.topjohnwu.magisk.di.koinModules import com.topjohnwu.magisk.di.koinModules
import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.net.Networking import com.topjohnwu.magisk.extensions.unwrap
import com.topjohnwu.magisk.utils.LocaleManager import com.topjohnwu.magisk.utils.ResourceMgr
import com.topjohnwu.magisk.utils.RootUtils import com.topjohnwu.magisk.utils.RootInit
import com.topjohnwu.magisk.utils.isRunningAsStub
import com.topjohnwu.magisk.utils.wrap
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
@ -27,7 +29,7 @@ open class App : Application() {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX) Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
Shell.Config.verboseLogging(BuildConfig.DEBUG) Shell.Config.verboseLogging(BuildConfig.DEBUG)
Shell.Config.addInitializers(RootUtils::class.java) Shell.Config.addInitializers(RootInit::class.java)
Shell.Config.setTimeout(2) Shell.Config.setTimeout(2)
Room.setFactory { Room.setFactory {
when (it) { when (it) {
@ -39,24 +41,42 @@ open class App : Application() {
} }
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {
super.attachBaseContext(base) // Basic setup
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
MultiDex.install(base) MultiDex.install(base)
Timber.plant(Timber.DebugTree()) 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 { startKoin {
androidContext(this@App) androidContext(baseContext)
modules(koinModules) modules(koinModules)
} }
ResourceMgr.reload()
app.registerActivityLifecycleCallbacks(get<ActivityTracker>())
}
registerActivityLifecycleCallbacks(get<ActivityTracker>()) // This is required as some platforms expect ContextImpl
override fun getBaseContext(): Context {
Networking.init(base) return super.getBaseContext().unwrap()
LocaleManager.setLocale(this)
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
ResourceMgr.reload(newConfig)
if (!isRunningAsStub)
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
LocaleManager.setLocale(this)
} }
} }

View File

@ -13,8 +13,8 @@ object Const {
// Versions // Versions
const val SNET_EXT_VER = 13 const val SNET_EXT_VER = 13
const val SNET_REVISION = "5adbc435ce93ded953c30ebe587edfd50b5503bc" const val SNET_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77" const val BOOTCTL_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
// Misc // Misc
const val ANDROID_MANIFEST = "AndroidManifest.xml" const val ANDROID_MANIFEST = "AndroidManifest.xml"

View 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)
}
}

View 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
}

View File

@ -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)
}
}
}

View 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?)
}

View 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())
}
}

View File

@ -1,4 +1,4 @@
package com.topjohnwu.magisk.model.worker package com.topjohnwu.magisk.base
import android.content.Context import android.content.Context
import android.net.Network import android.net.Network

View File

@ -1,24 +1,23 @@
package com.topjohnwu.magisk.ui.base package com.topjohnwu.magisk.base.viewmodel
import android.app.Activity import android.app.Activity
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.skoumal.teanity.extensions.doOnSubscribeUi import com.topjohnwu.magisk.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.get 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.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.utils.KObservableField
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.PublishSubject
abstract class MagiskViewModel( abstract class BaseViewModel(
initialState: State = State.LOADING initialState: State = State.LOADING
) : LoadingViewModel(initialState) { ) : LoadingViewModel(initialState) {
val isConnected = KObservableField(true) val isConnected = KObservableField(false)
init { init {
ReactiveNetwork.observeNetworkConnectivity(get()) ReactiveNetwork.observeNetworkConnectivity(get())

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -19,10 +19,10 @@ interface GithubRawServices {
@GET("$MAGISK_FILES/master/beta.json") @GET("$MAGISK_FILES/master/beta.json")
fun fetchBetaUpdate(): Single<UpdateInfo> fun fetchBetaUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/canary_builds/release.json") @GET("$MAGISK_FILES/canary/release.json")
fun fetchCanaryUpdate(): Single<UpdateInfo> fun fetchCanaryUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/canary_builds/canary.json") @GET("$MAGISK_FILES/canary/debug.json")
fun fetchCanaryDebugUpdate(): Single<UpdateInfo> fun fetchCanaryDebugUpdate(): Single<UpdateInfo>
@GET @GET

View File

@ -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())
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -7,7 +7,7 @@ import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.preference.PreferenceManager 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.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module

View File

@ -1,11 +1,18 @@
package com.topjohnwu.magisk.di package com.topjohnwu.magisk.di
import android.content.Context
import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.network.GithubApiServices import com.topjohnwu.magisk.data.network.GithubApiServices
import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.net.Networking
import com.topjohnwu.magisk.net.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.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import org.koin.dsl.module import org.koin.dsl.module
@ -16,14 +23,15 @@ import retrofit2.converter.scalars.ScalarsConverterFactory
import se.ansman.kotshi.KotshiJsonAdapterFactory import se.ansman.kotshi.KotshiJsonAdapterFactory
val networkingModule = module { val networkingModule = module {
single { createOkHttpClient() } single { createOkHttpClient(get()) }
single { createMoshiConverterFactory() } single { createRetrofit(get()) }
single { createRetrofit(get(), get()) }
single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) } single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_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() val builder = OkHttpClient.Builder()
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
@ -33,6 +41,10 @@ fun createOkHttpClient(): OkHttpClient {
builder.addInterceptor(httpLoggingInterceptor) builder.addInterceptor(httpLoggingInterceptor)
} }
if (!Networking.init(context)) {
builder.sslSocketFactory(NoSSLv3SocketFactory())
}
return builder.build() return builder.build()
} }
@ -43,13 +55,10 @@ fun createMoshiConverterFactory(): MoshiConverterFactory {
return MoshiConverterFactory.create(moshi) return MoshiConverterFactory.create(moshi)
} }
fun createRetrofit( fun createRetrofit(okHttpClient: OkHttpClient): Retrofit.Builder {
okHttpClient: OkHttpClient,
converterFactory: MoshiConverterFactory
): Retrofit.Builder {
return Retrofit.Builder() return Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(converterFactory) .addConverterFactory(createMoshiConverterFactory())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttpClient) .client(okHttpClient)
} }
@ -63,3 +72,12 @@ inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseU
.build() .build()
.create(T::class.java) .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()
}

View File

@ -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))
}

View File

@ -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()

View File

@ -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)

View 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) })

View 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)
}

View File

@ -1,6 +1,8 @@
package com.topjohnwu.magisk.extensions package com.topjohnwu.magisk.extensions
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo 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.pm.PackageManager.* import android.content.pm.PackageManager.*
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Build
import android.provider.OpenableColumns 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.FileProvider
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.currentLocale import com.topjohnwu.magisk.utils.currentLocale
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.util.*
val packageName: String get() = get<Context>().packageName 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.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 { fun File.provide(context: Context = get()): Uri {
return FileProvider.getUriForFile(context, context.packageName + ".provider", this) return FileProvider.getUriForFile(context, context.packageName + ".provider", this)
} }
@ -119,3 +230,48 @@ fun ApplicationInfo.getLabel(pm: PackageManager): String {
return loadLabel(pm).toString() 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
}

View File

@ -1,6 +1,6 @@
package com.topjohnwu.magisk.extensions package com.topjohnwu.magisk.extensions
import com.skoumal.teanity.util.KObservableField import com.topjohnwu.magisk.utils.KObservableField
fun KObservableField<Boolean>.toggle() { fun KObservableField<Boolean>.toggle() {

View File

@ -2,8 +2,7 @@ package com.topjohnwu.magisk.extensions
import androidx.collection.SparseArrayCompat import androidx.collection.SparseArrayCompat
import androidx.databinding.ObservableList import androidx.databinding.ObservableList
import com.skoumal.teanity.extensions.subscribeK import com.topjohnwu.magisk.utils.DiffObservableList
import com.skoumal.teanity.util.DiffObservableList
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
fun <T> MutableList<T>.update(newList: List<T>) { fun <T> MutableList<T>.update(newList: List<T>) {

View File

@ -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) })

View File

@ -2,7 +2,7 @@ package com.topjohnwu.magisk.model.binding
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView 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 com.topjohnwu.magisk.model.entity.recycler.LenientRvItem
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter

View File

@ -10,6 +10,7 @@ import androidx.core.app.NotificationCompat
import com.topjohnwu.magisk.ClassMap import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.extensions.chooser import com.topjohnwu.magisk.extensions.chooser
import com.topjohnwu.magisk.extensions.exists
import com.topjohnwu.magisk.extensions.provide import com.topjohnwu.magisk.extensions.provide
import com.topjohnwu.magisk.model.entity.internal.Configuration.* import com.topjohnwu.magisk.model.entity.internal.Configuration.*
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
@ -17,6 +18,7 @@ import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.* import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
import com.topjohnwu.magisk.ui.flash.FlashActivity import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.magisk.utils.APKInstall import com.topjohnwu.magisk.utils.APKInstall
import org.koin.core.get
import java.io.File import java.io.File
import kotlin.random.Random.Default.nextInt import kotlin.random.Random.Default.nextInt
@ -76,8 +78,14 @@ open class DownloadService : RemoteFileService() {
private fun NotificationCompat.Builder.addActionsInternal(subject: Magisk) private fun NotificationCompat.Builder.addActionsInternal(subject: Magisk)
= when (val conf = subject.configuration) { = when (val conf = subject.configuration) {
Download -> addAction(0, R.string.download_open_parent, fileIntent(subject.file.parentFile!!)) Download -> this.apply {
.addAction(0, R.string.download_open_self, fileIntent(subject.file)) 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)) Uninstall -> setContentIntent(FlashActivity.uninstallIntent(context, subject.file))
is Flash -> setContentIntent(FlashActivity.flashIntent(context, subject.file, conf is Secondary)) is Flash -> setContentIntent(FlashActivity.flashIntent(context, subject.file, conf is Secondary))
is Patch -> setContentIntent(FlashActivity.patchIntent(context, subject.file, conf.fileUri)) 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) private fun NotificationCompat.Builder.addActionsInternal(subject: Module)
= when (subject.configuration) { = when (subject.configuration) {
Download -> addAction(0, R.string.download_open_parent, fileIntent(subject.file.parentFile!!)) Download -> this.apply {
.addAction(0, R.string.download_open_self, fileIntent(subject.file)) 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)) is Flash -> setContentIntent(FlashActivity.installIntent(context, subject.file))
else -> this else -> this
} }
@ -115,7 +129,6 @@ open class DownloadService : RemoteFileService() {
.setDataAndType(file.provide(this), file.type) .setDataAndType(file.provide(this), file.type)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.chooser()
} }
class Builder { class Builder {

View File

@ -5,13 +5,13 @@ import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.ClassMap import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R 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.Restore
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Upgrade import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Upgrade
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.ui.SplashActivity import com.topjohnwu.magisk.ui.SplashActivity
import com.topjohnwu.magisk.utils.DynamicClassLoader
import com.topjohnwu.magisk.utils.PatchAPK import com.topjohnwu.magisk.utils.PatchAPK
import com.topjohnwu.magisk.utils.RootUtils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -54,7 +54,7 @@ private fun RemoteFileService.restore(apk: File, id: Int) {
if (Shell.su("pm install $apk").exec().isSuccess) { if (Shell.su("pm install $apk").exec().isSuccess) {
val component = ComponentName(BuildConfig.APPLICATION_ID, val component = ComponentName(BuildConfig.APPLICATION_ID,
ClassMap.get<Class<*>>(SplashActivity::class.java).name) ClassMap.get<Class<*>>(SplashActivity::class.java).name)
RootUtils.rmAndLaunch(packageName, component) Utils.rmAndLaunch(packageName, component)
} }
} }

View File

@ -1,15 +1,16 @@
package com.topjohnwu.magisk.model.download package com.topjohnwu.magisk.model.download
import android.app.Notification import android.app.Notification
import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import com.topjohnwu.magisk.base.BaseService
import org.koin.core.KoinComponent
import java.util.* import java.util.*
import kotlin.random.Random.Default.nextInt import kotlin.random.Random.Default.nextInt
abstract class NotificationService : Service() { abstract class NotificationService : BaseService(), KoinComponent {
abstract val defaultNotification: NotificationCompat.Builder abstract val defaultNotification: NotificationCompat.Builder

View File

@ -3,11 +3,11 @@ package com.topjohnwu.magisk.model.download
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.di.NullActivity import com.topjohnwu.magisk.di.NullActivity
import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.* import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*

View File

@ -1,3 +0,0 @@
package com.topjohnwu.magisk.model.entity
data class Version(val version: String, val versionCode: Int)

View File

@ -48,7 +48,7 @@ class Module(path: String) : BaseModule() {
} }
if (name.isEmpty()) { if (name.isEmpty()) {
name = id; name = id
} }
} }
@ -65,7 +65,7 @@ class Module(path: String) : BaseModule() {
val module = Module(Const.MAGISK_PATH + "/" + file.name) val module = Module(Const.MAGISK_PATH + "/" + file.name)
moduleList.add(module) moduleList.add(module)
} }
return moduleList return moduleList.sortedBy { it.name }
} }
} }
} }

View File

@ -1,17 +1,17 @@
package com.topjohnwu.magisk.model.entity.recycler 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.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.toggle import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.HideAppInfo import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.magisk.model.entity.state.IndeterminateState import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent 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>) : class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
ComparableRvItem<HideRvItem>() { ComparableRvItem<HideRvItem>() {

View File

@ -2,7 +2,7 @@ package com.topjohnwu.magisk.model.entity.recycler
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView 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 * This item addresses issues where enclosing recycler has to be invalidated or generally

View File

@ -1,14 +1,14 @@
package com.topjohnwu.magisk.model.entity.recycler 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.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.timeFormatMedium import com.topjohnwu.magisk.extensions.timeFormatMedium
import com.topjohnwu.magisk.extensions.toTime import com.topjohnwu.magisk.extensions.toTime
import com.topjohnwu.magisk.extensions.toggle import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskLog import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
class LogRvItem : ComparableRvItem<LogRvItem>() { class LogRvItem : ComparableRvItem<LogRvItem>() {
override val layoutRes: Int = R.layout.item_page_log override val layoutRes: Int = R.layout.item_page_log

View File

@ -2,14 +2,14 @@ package com.topjohnwu.magisk.model.entity.recycler
import android.content.res.Resources import android.content.res.Resources
import androidx.annotation.StringRes 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.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.toggle import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.module.Module import com.topjohnwu.magisk.model.entity.module.Module
import com.topjohnwu.magisk.model.entity.module.Repo import com.topjohnwu.magisk.model.entity.module.Repo
import com.topjohnwu.magisk.utils.KObservableField
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() { class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {

View File

@ -1,16 +1,16 @@
package com.topjohnwu.magisk.model.entity.recycler package com.topjohnwu.magisk.model.entity.recycler
import android.graphics.drawable.Drawable 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.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.toggle import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskPolicy import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.events.PolicyEnableEvent import com.topjohnwu.magisk.model.events.PolicyEnableEvent
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() { class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {

View File

@ -1,7 +1,7 @@
package com.topjohnwu.magisk.model.entity.recycler package com.topjohnwu.magisk.model.entity.recycler
import com.skoumal.teanity.databinding.ComparableRvItem
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
class SectionRvItem(val text: String) : ComparableRvItem<SectionRvItem>() { class SectionRvItem(val text: String) : ComparableRvItem<SectionRvItem>() {
override val layoutRes: Int = R.layout.item_section override val layoutRes: Int = R.layout.item_section

View File

@ -1,7 +1,7 @@
package com.topjohnwu.magisk.model.entity.recycler package com.topjohnwu.magisk.model.entity.recycler
import com.skoumal.teanity.databinding.ComparableRvItem
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
class SpinnerRvItem(val item: String) : ComparableRvItem<SpinnerRvItem>() { class SpinnerRvItem(val item: String) : ComparableRvItem<SpinnerRvItem>() {

View File

@ -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)
}
}
}

View File

@ -1,10 +1,10 @@
package com.topjohnwu.magisk.model.events 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.MagiskPolicy
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
import com.topjohnwu.magisk.utils.RxBus
class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event

View File

@ -0,0 +1,5 @@
package com.topjohnwu.magisk.model.events
class SimpleViewEvent(
val event: Int
) : ViewEvent()

View File

@ -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)
}

View File

@ -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)
}
}
}
}

View File

@ -12,6 +12,16 @@ import com.topjohnwu.magisk.model.entity.module.Repo
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
import io.reactivex.subjects.PublishSubject 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() data class OpenLinkEvent(val url: String) : ViewEvent()

View File

@ -5,12 +5,14 @@ import androidx.annotation.AnimRes
import androidx.annotation.AnimatorRes import androidx.annotation.AnimatorRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.skoumal.teanity.viewevents.NavigationDslMarker import com.topjohnwu.magisk.model.events.ViewEvent
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.model.events.ActivityExecutor import com.topjohnwu.magisk.model.events.ActivityExecutor
import com.topjohnwu.magisk.redesign.compat.CompatActivity import com.topjohnwu.magisk.redesign.compat.CompatActivity
import kotlin.reflect.KClass import kotlin.reflect.KClass
@DslMarker
annotation class NavigationDslMarker
class MagiskNavigationEvent( class MagiskNavigationEvent(
val navDirections: MagiskNavDirectionsBuilder, val navDirections: MagiskNavDirectionsBuilder,
val navOptions: MagiskNavOptions, val navOptions: MagiskNavOptions,

View File

@ -1,15 +1,14 @@
package com.topjohnwu.magisk.model.receiver package com.topjohnwu.magisk.model.receiver
import android.content.BroadcastReceiver import android.content.ContextWrapper
import android.content.Context
import android.content.Intent import android.content.Intent
import com.topjohnwu.magisk.ClassMap import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Info import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.base.BaseReceiver
import com.topjohnwu.magisk.data.database.PolicyDao import com.topjohnwu.magisk.data.database.PolicyDao
import com.topjohnwu.magisk.data.database.base.su import com.topjohnwu.magisk.data.database.base.su
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.reboot import com.topjohnwu.magisk.extensions.reboot
import com.topjohnwu.magisk.model.download.DownloadService import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.ManagerJson 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.Notifications
import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import org.koin.core.inject
open class GeneralReceiver : BroadcastReceiver() { open class GeneralReceiver : BaseReceiver() {
private val policyDB: PolicyDao by inject() private val policyDB: PolicyDao by inject()
@ -36,7 +36,7 @@ open class GeneralReceiver : BroadcastReceiver() {
return intent.data?.encodedSchemeSpecificPart.orEmpty() return intent.data?.encodedSchemeSpecificPart.orEmpty()
} }
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: ContextWrapper, intent: Intent?) {
intent ?: return intent ?: return
when (intent.action ?: return) { when (intent.action ?: return) {
Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> { Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> {

View File

@ -3,9 +3,9 @@ package com.topjohnwu.magisk.model.update
import androidx.work.ListenableWorker import androidx.work.ListenableWorker
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Info import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.base.DelegateWorker
import com.topjohnwu.magisk.data.repository.MagiskRepository import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.model.worker.DelegateWorker
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell

View File

@ -2,11 +2,11 @@ package com.topjohnwu.magisk.tasks
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.extensions.fileName import com.topjohnwu.magisk.extensions.fileName
import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.readUri import com.topjohnwu.magisk.extensions.readUri
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.utils.unzip import com.topjohnwu.magisk.utils.unzip
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import io.reactivex.Single import io.reactivex.Single

View File

@ -6,7 +6,6 @@ import android.os.Build
import android.text.TextUtils import android.text.TextUtils
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Info import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.data.network.GithubRawServices 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.TarInputStream
import org.kamranzafar.jtar.TarOutputStream import org.kamranzafar.jtar.TarOutputStream
import timber.log.Timber 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.nio.ByteBuffer
import java.util.*
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
@ -40,7 +41,7 @@ abstract class MagiskInstaller {
private val console: MutableList<String> private val console: MutableList<String>
private val logs: MutableList<String> private val logs: MutableList<String>
private var isTar = false private var tarOut: TarOutputStream? = null
private val service: GithubRawServices by inject() private val service: GithubRawServices by inject()
private val context: Context by inject() private val context: Context by inject()
@ -151,7 +152,9 @@ abstract class MagiskInstaller {
private fun handleTar(input: InputStream) { private fun handleTar(input: InputStream) {
console.add("- Processing tar file") console.add("- Processing tar file")
var vbmeta = false 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 lateinit var entry: TarEntry
while (tarIn.nextEntry?.let { entry = it } != null) { while (tarIn.nextEntry?.let { entry = it } != null) {
if (entry.name.contains("boot.img") || entry.name.contains("recovery.img")) { if (entry.name.contains("boot.img") || entry.name.contains("recovery.img")) {
@ -215,8 +218,7 @@ abstract class MagiskInstaller {
return false return false
} }
it.reset() it.reset()
if (Arrays.equals(magic, "ustar".toByteArray())) { if (magic.contentEquals("ustar".toByteArray())) {
isTar = true
destFile = File(Config.downloadDirectory, "magisk_patched.tar") destFile = File(Config.downloadDirectory, "magisk_patched.tar")
handleTar(it) handleTar(it)
} else { } else {
@ -293,15 +295,13 @@ abstract class MagiskInstaller {
protected fun storeBoot(): Boolean { protected fun storeBoot(): Boolean {
val patched = SuFile.open(installDir, "new-boot.img") val patched = SuFile.open(installDir, "new-boot.img")
try { try {
val os: OutputStream val os = tarOut?.let {
if (isTar) { it.putNextEntry(newEntry(
os = TarOutputStream(destFile, true)
os.putNextEntry(newEntry(
if (srcBoot.contains("recovery")) "recovery.img" else "boot.img", if (srcBoot.contains("recovery")) "recovery.img" else "boot.img",
patched.length())) patched.length()))
} else { tarOut = null
os = destFile.outputStream() it
} } ?: destFile.outputStream()
patched.suInputStream().use { it.copyTo(os); os.close() } patched.suInputStream().use { it.copyTo(os); os.close() }
} catch (e: IOException) { } catch (e: IOException) {
console.add("! Failed to output to $destFile") console.add("! Failed to output to $destFile")

View File

@ -4,15 +4,24 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.fragment.app.Fragment 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.ClassMap
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const.Key.OPEN_SECTION import com.topjohnwu.magisk.Const.Key.OPEN_SECTION
import com.topjohnwu.magisk.Info import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.ActivityMainBinding 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.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.hide.MagiskHideFragment
import com.topjohnwu.magisk.ui.home.HomeFragment import com.topjohnwu.magisk.ui.home.HomeFragment
import com.topjohnwu.magisk.ui.log.LogFragment import com.topjohnwu.magisk.ui.log.LogFragment
@ -23,15 +32,22 @@ import com.topjohnwu.magisk.ui.superuser.SuperuserFragment
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
import kotlin.reflect.KClass import kotlin.reflect.KClass
open class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>(), Navigator,
open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() { FragNavController.RootFragmentListener, FragNavController.TransactionListener {
override val layoutRes: Int = R.layout.activity_main override val layoutRes: Int = R.layout.activity_main
override val viewModel: MainViewModel by viewModel() override val viewModel: MainViewModel by viewModel()
override val navHostId: Int = R.id.main_nav_host private val navHostId: Int = R.id.main_nav_host
override val defaultPosition: Int = 0 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( override val baseFragments: List<KClass<out Fragment>> = listOf(
HomeFragment::class, HomeFragment::class,
@ -43,10 +59,6 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
SettingsFragment::class SettingsFragment::class
) )
/*override fun getDarkTheme(): Int {
return R.style.AppTheme_Dark
}*/
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
if (!SplashActivity.DONE) { if (!SplashActivity.DONE) {
startActivity(Intent(this, ClassMap[SplashActivity::class.java])) startActivity(Intent(this, ClassMap[SplashActivity::class.java]))
@ -54,6 +66,13 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
} }
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
navigationController.apply {
rootFragmentListener = this@MainActivity
transactionListener = this@MainActivity
initialize(defaultPosition, savedInstanceState)
}
checkHideSection() checkHideSection()
setSupportActionBar(binding.mainInclude.mainToolbar) 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?) { override fun setTitle(title: CharSequence?) {
supportActionBar?.title = title supportActionBar?.title = title
} }
@ -76,25 +100,46 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
supportActionBar?.setTitle(titleId) 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() { override fun onBackPressed() {
if (binding.drawerLayout.isDrawerOpen(binding.navView)) { if (binding.drawerLayout.isDrawerOpen(binding.navView)) {
binding.drawerLayout.closeDrawer(binding.navView) binding.drawerLayout.closeDrawer(binding.navView)
} else { } 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 = menu.findItem(R.id.superuserFragment).isVisible =
Utils.showSuperUser() 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
} }

View File

@ -2,11 +2,11 @@ package com.topjohnwu.magisk.ui
import android.view.MenuItem import android.view.MenuItem
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.model.navigation.Navigation 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() fun navPressed() = Navigation.Main.OPEN_NAV.publish()

View File

@ -1,17 +1,24 @@
package com.topjohnwu.magisk.ui package com.topjohnwu.magisk.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.topjohnwu.magisk.* import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.model.navigation.Navigation import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.wrap
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -9,15 +9,21 @@ import androidx.core.net.toUri
import com.topjohnwu.magisk.ClassMap import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.databinding.ActivityFlashBinding 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.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import java.io.File import java.io.File
open class FlashActivity : MagiskActivity<FlashViewModel, ActivityFlashBinding>() { open class FlashActivity : BaseActivity<FlashViewModel, ActivityFlashBinding>() {
override val layoutRes: Int = R.layout.activity_flash override val layoutRes: Int = R.layout.activity_flash
override val themeRes: Int = R.style.MagiskTheme_Flashing
override val viewModel: FlashViewModel by viewModel { override val viewModel: FlashViewModel by viewModel {
val uri = intent.data ?: let { finish(); Uri.EMPTY } val uri = intent.data ?: let { finish(); Uri.EMPTY }
val additionalUri = intent.getParcelableExtra(Const.Key.FLASH_DATA) ?: uri val additionalUri = intent.getParcelableExtra(Const.Key.FLASH_DATA) ?: uri
@ -37,6 +43,21 @@ open class FlashActivity : MagiskActivity<FlashViewModel, ActivityFlashBinding>(
super.onBackPressed() 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 { companion object {
private fun intent(context: Context) = Intent(context, ClassMap[FlashActivity::class.java]) private fun intent(context: Context) = Intent(context, ClassMap[FlashActivity::class.java])

View File

@ -7,21 +7,20 @@ import android.net.Uri
import android.os.Handler import android.os.Handler
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import androidx.databinding.ObservableArrayList 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.BR
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.* import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem 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.FlashResultListener
import com.topjohnwu.magisk.model.flash.Flashing import com.topjohnwu.magisk.model.flash.Flashing
import com.topjohnwu.magisk.model.flash.Patching 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 com.topjohnwu.superuser.Shell
import me.tatarka.bindingcollectionadapter2.ItemBinding import me.tatarka.bindingcollectionadapter2.ItemBinding
import java.io.File import java.io.File
@ -32,7 +31,7 @@ class FlashViewModel(
installer: Uri, installer: Uri,
uri: Uri, uri: Uri,
private val resources: Resources private val resources: Resources
) : MagiskViewModel(), FlashResultListener { ) : BaseViewModel(), FlashResultListener {
val canShowReboot = Shell.rootAccess() val canShowReboot = Shell.rootAccess()
val showRestartTitle = KObservableField(false) val showRestartTitle = KObservableField(false)

View File

@ -1,28 +1,28 @@
package com.topjohnwu.magisk.ui.hide package com.topjohnwu.magisk.ui.hide
import android.content.pm.ApplicationInfo 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.BR
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.repository.MagiskRepository import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.toSingle import com.topjohnwu.magisk.extensions.toSingle
import com.topjohnwu.magisk.extensions.update import com.topjohnwu.magisk.extensions.update
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
import com.topjohnwu.magisk.model.entity.recycler.HideRvItem import com.topjohnwu.magisk.model.entity.recycler.HideRvItem
import com.topjohnwu.magisk.model.entity.state.IndeterminateState import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent 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 me.tatarka.bindingcollectionadapter2.OnItemBind
import timber.log.Timber import timber.log.Timber
class HideViewModel( class HideViewModel(
private val magiskRepo: MagiskRepository, private val magiskRepo: MagiskRepository,
rxBus: RxBus rxBus: RxBus
) : MagiskViewModel() { ) : BaseViewModel() {
val query = KObservableField("") val query = KObservableField("")
val isShowSystem = KObservableField(false) val isShowSystem = KObservableField(false)

View File

@ -6,11 +6,11 @@ import android.view.MenuItem
import android.widget.SearchView import android.widget.SearchView
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentMagiskHideBinding import com.topjohnwu.magisk.databinding.FragmentMagiskHideBinding
import com.topjohnwu.magisk.ui.base.MagiskFragment
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBinding>(), class MagiskHideFragment : BaseFragment<HideViewModel, FragmentMagiskHideBinding>(),
SearchView.OnQueryTextListener { SearchView.OnQueryTextListener {
override val layoutRes: Int = R.layout.fragment_magisk_hide override val layoutRes: Int = R.layout.fragment_magisk_hide

View File

@ -1,31 +1,31 @@
package com.topjohnwu.magisk.ui.home package com.topjohnwu.magisk.ui.home
import android.content.Context 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.BuildConfig
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Info import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.data.repository.MagiskRepository import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.databinding.FragmentMagiskBinding 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.extensions.writeTo
import com.topjohnwu.magisk.model.events.* 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.utils.SafetyNetHelper
import com.topjohnwu.magisk.view.MarkDownWindow import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.magisk.view.dialogs.* import com.topjohnwu.magisk.view.dialogs.*
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import dalvik.system.DexFile import dalvik.system.DexFile
import io.reactivex.Completable import io.reactivex.Completable
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import java.io.File import java.io.File
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(), class HomeFragment : BaseFragment<HomeViewModel, FragmentMagiskBinding>(),
SafetyNetHelper.Callback { SafetyNetHelper.Callback {
override val layoutRes: Int = R.layout.fragment_magisk 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 magiskRepo: MagiskRepository by inject()
private val EXT_APK by lazy { File("${activity.filesDir.parent}/snet", "snet.jar") } 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 onResponse(responseCode: Int) = viewModel.finishSafetyNetCheck(responseCode)
override fun onEventDispatched(event: ViewEvent) { override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event) super.onEventDispatched(event)
when (event) { when (event) {
is OpenLinkEvent -> openLink(event.url) is OpenLinkEvent -> activity.openUrl(event.url)
is ManagerInstallEvent -> installManager() is ManagerInstallEvent -> installManager()
is MagiskInstallEvent -> installMagisk() is MagiskInstallEvent -> installMagisk()
is UninstallEvent -> uninstall() is UninstallEvent -> uninstall()
@ -62,7 +63,7 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
return return
} }
MagiskInstallDialog(requireActivity() as MagiskActivity<*, *>).show() MagiskInstallDialog(requireActivity() as BaseActivity<*, *>).show()
} }
private fun installManager() = ManagerInstallDialog(requireActivity()).show() private fun installManager() = ManagerInstallDialog(requireActivity()).show()
@ -94,7 +95,7 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
private fun updateSafetyNet(dieOnError: Boolean) { private fun updateSafetyNet(dieOnError: Boolean) {
Completable.fromAction { Completable.fromAction {
val loader = DynamicClassLoader(EXT_APK) 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 // Scan through the dex and find our helper class
var helperClass: Class<*>? = null var helperClass: Class<*>? = null

View File

@ -1,21 +1,16 @@
package com.topjohnwu.magisk.ui.home package com.topjohnwu.magisk.ui.home
import android.content.pm.PackageManager 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.*
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.repository.MagiskRepository import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.extensions.packageName
import com.topjohnwu.magisk.extensions.res
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.events.* import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.model.observer.Observer 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.magisk.utils.SafetyNetHelper
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import io.reactivex.Completable
enum class SafetyNetState { enum class SafetyNetState {
LOADING, PASS, FAILED, IDLE LOADING, PASS, FAILED, IDLE
@ -31,7 +26,7 @@ enum class MagiskItem {
class HomeViewModel( class HomeViewModel(
private val magiskRepo: MagiskRepository private val magiskRepo: MagiskRepository
) : MagiskViewModel(State.LOADED) { ) : BaseViewModel(State.LOADED) {
val hasGMS = runCatching { val hasGMS = runCatching {
get<PackageManager>().getPackageInfo("com.google.android.gms", 0); true get<PackageManager>().getPackageInfo("com.google.android.gms", 0); true
@ -43,10 +38,7 @@ class HomeViewModel(
val isKeepVerity = KObservableField(Info.keepVerity) val isKeepVerity = KObservableField(Info.keepVerity)
val isRecovery = KObservableField(Info.recovery) val isRecovery = KObservableField(Info.recovery)
private val _magiskState = KObservableField(MagiskState.LOADING) val magiskState = KObservableField(MagiskState.LOADING)
val magiskState = Observer(_magiskState, isConnected) {
if (isConnected.value) _magiskState.value else MagiskState.UP_TO_DATE
}
val magiskStateText = Observer(magiskState) { val magiskStateText = Observer(magiskState) {
when (magiskState.value) { when (magiskState.value) {
MagiskState.NOT_INSTALLED -> R.string.magisk_version_error.res() MagiskState.NOT_INSTALLED -> R.string.magisk_version_error.res()
@ -179,24 +171,28 @@ class HomeViewModel(
} }
fun refresh() { fun refresh() {
refreshVersions() hasRoot.value = Shell.rootAccess()
magiskRepo.fetchUpdate() val fetchUpdate = if (isConnected.value)
magiskRepo.fetchUpdate().ignoreElement()
else
Completable.complete()
Completable.fromAction {
Info.loadMagiskInfo()
}.andThen(fetchUpdate)
.applyViewModel(this) .applyViewModel(this)
.doOnSubscribeUi { .doOnSubscribeUi {
_magiskState.value = MagiskState.LOADING magiskState.value = MagiskState.LOADING
_managerState.value = MagiskState.LOADING _managerState.value = MagiskState.LOADING
ctsState.value = SafetyNetState.IDLE ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE basicIntegrityState.value = SafetyNetState.IDLE
safetyNetTitle.value = R.string.safetyNet_check_text safetyNetTitle.value = R.string.safetyNet_check_text
} }.subscribeK {
.subscribeK {
updateSelf() updateSelf()
ensureEnv() ensureEnv()
refreshVersions() refreshVersions()
} }
hasRoot.value = Shell.rootAccess()
} }
private fun refreshVersions() { private fun refreshVersions() {
@ -211,7 +207,7 @@ class HomeViewModel(
} }
private fun updateSelf() { private fun updateSelf() {
_magiskState.value = when (Info.magiskVersionCode) { magiskState.value = when (Info.magiskVersionCode) {
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED
!in Info.remote.magisk.versionCode..Int.MAX_VALUE -> MagiskState.OBSOLETE !in Info.remote.magisk.versionCode..Int.MAX_VALUE -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE else -> MagiskState.UP_TO_DATE

View File

@ -6,14 +6,14 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentLogBinding import com.topjohnwu.magisk.databinding.FragmentLogBinding
import com.topjohnwu.magisk.model.events.PageChangedEvent 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 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 layoutRes: Int = R.layout.fragment_log
override val viewModel: LogViewModel by viewModel() override val viewModel: LogViewModel by viewModel()

View File

@ -1,25 +1,25 @@
package com.topjohnwu.magisk.ui.log package com.topjohnwu.magisk.ui.log
import android.content.res.Resources 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.BR
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.repository.LogRepository 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.binding.BindingAdapter
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
import com.topjohnwu.magisk.model.entity.recycler.LogItemRvItem import com.topjohnwu.magisk.model.entity.recycler.LogItemRvItem
import com.topjohnwu.magisk.model.entity.recycler.LogRvItem import com.topjohnwu.magisk.model.entity.recycler.LogRvItem
import com.topjohnwu.magisk.model.entity.recycler.MagiskLogRvItem import com.topjohnwu.magisk.model.entity.recycler.MagiskLogRvItem
import com.topjohnwu.magisk.model.events.PageChangedEvent 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 com.topjohnwu.superuser.Shell
import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter
import me.tatarka.bindingcollectionadapter2.OnItemBind import me.tatarka.bindingcollectionadapter2.OnItemBind
@ -30,7 +30,7 @@ import java.util.*
class LogViewModel( class LogViewModel(
private val resources: Resources, private val resources: Resources,
private val logRepo: LogRepository private val logRepo: LogRepository
) : MagiskViewModel(), BindingViewPagerAdapter.PageTitles<ComparableRvItem<*>> { ) : BaseViewModel(), BindingViewPagerAdapter.PageTitles<ComparableRvItem<*>> {
val itemsAdapter = BindingAdapter() val itemsAdapter = BindingAdapter()
val items = DiffObservableList(ComparableRvItem.callback) val items = DiffObservableList(ComparableRvItem.callback)

View File

@ -1,17 +1,12 @@
package com.topjohnwu.magisk.ui.module package com.topjohnwu.magisk.ui.module
import android.content.res.Resources 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.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.database.RepoDao import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.extensions.toSingle import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.update import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.model.entity.module.Module import com.topjohnwu.magisk.model.entity.module.Module
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem 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.OpenChangelogEvent
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
import com.topjohnwu.magisk.tasks.RepoUpdater 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.Single
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import me.tatarka.bindingcollectionadapter2.OnItemBind import me.tatarka.bindingcollectionadapter2.OnItemBind
@ -29,7 +25,7 @@ class ModuleViewModel(
private val resources: Resources, private val resources: Resources,
private val repoUpdater: RepoUpdater, private val repoUpdater: RepoUpdater,
private val repoDB: RepoDao private val repoDB: RepoDao
) : MagiskViewModel() { ) : BaseViewModel() {
val query = KObservableField("") val query = KObservableField("")

View File

@ -8,19 +8,19 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.ClassMap import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentModulesBinding import com.topjohnwu.magisk.databinding.FragmentModulesBinding
import com.topjohnwu.magisk.extensions.reboot import com.topjohnwu.magisk.extensions.reboot
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent 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.magisk.ui.flash.FlashActivity
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.sharedViewModel 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 layoutRes: Int = R.layout.fragment_modules
override val viewModel: ModuleViewModel by sharedViewModel() override val viewModel: ModuleViewModel by sharedViewModel()

View File

@ -6,9 +6,9 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.widget.SearchView import android.widget.SearchView
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentReposBinding import com.topjohnwu.magisk.databinding.FragmentReposBinding
import com.topjohnwu.magisk.model.download.DownloadService import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.internal.Configuration 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.entity.module.Repo
import com.topjohnwu.magisk.model.events.InstallModuleEvent import com.topjohnwu.magisk.model.events.InstallModuleEvent
import com.topjohnwu.magisk.model.events.OpenChangelogEvent import com.topjohnwu.magisk.model.events.OpenChangelogEvent
import com.topjohnwu.magisk.ui.base.MagiskFragment import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.view.MarkDownWindow import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(), class ReposFragment : BaseFragment<ModuleViewModel, FragmentReposBinding>(),
SearchView.OnQueryTextListener { SearchView.OnQueryTextListener {
override val layoutRes: Int = R.layout.fragment_repos override val layoutRes: Int = R.layout.fragment_repos

View File

@ -13,21 +13,20 @@ import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.SwitchPreferenceCompat 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.BuildConfig
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BasePreferenceFragment
import com.topjohnwu.magisk.data.database.RepoDao import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.databinding.CustomDownloadDialogBinding import com.topjohnwu.magisk.databinding.CustomDownloadDialogBinding
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.toLangTag import com.topjohnwu.magisk.extensions.toLangTag
import com.topjohnwu.magisk.model.download.DownloadService import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.internal.Configuration import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.observer.Observer import com.topjohnwu.magisk.model.observer.Observer
import com.topjohnwu.magisk.net.Networking import com.topjohnwu.magisk.net.Networking
import com.topjohnwu.magisk.ui.base.BasePreferenceFragment
import com.topjohnwu.magisk.utils.* import com.topjohnwu.magisk.utils.*
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
@ -204,7 +203,7 @@ class SettingsFragment : BasePreferenceFragment() {
Shell.su("magiskhide --disable").submit() Shell.su("magiskhide --disable").submit()
} }
Config.Key.LOCALE -> { Config.Key.LOCALE -> {
LocaleManager.setLocale(activity.application) ResourceMgr.reload()
activity.recreate() activity.recreate()
} }
Config.Key.CHECK_UPDATES -> Utils.scheduleUpdateCheck(activity) Config.Key.CHECK_UPDATES -> Utils.scheduleUpdateCheck(activity)
@ -233,7 +232,7 @@ class SettingsFragment : BasePreferenceFragment() {
val values = mutableListOf<String>() val values = mutableListOf<String>()
names.add( names.add(
LocaleManager.getString(defaultLocale, R.string.system_default) ResourceMgr.getString(defaultLocale, R.string.system_default)
) )
values.add("") values.add("")

View File

@ -1,12 +1,12 @@
package com.topjohnwu.magisk.ui.superuser package com.topjohnwu.magisk.ui.superuser
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentSuperuserBinding import com.topjohnwu.magisk.databinding.FragmentSuperuserBinding
import com.topjohnwu.magisk.ui.base.MagiskFragment
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
class SuperuserFragment : class SuperuserFragment :
MagiskFragment<SuperuserViewModel, FragmentSuperuserBinding>() { BaseFragment<SuperuserViewModel, FragmentSuperuserBinding>() {
override val layoutRes: Int = R.layout.fragment_superuser override val layoutRes: Int = R.layout.fragment_superuser
override val viewModel: SuperuserViewModel by viewModel() override val viewModel: SuperuserViewModel by viewModel()

View File

@ -2,22 +2,22 @@ package com.topjohnwu.magisk.ui.superuser
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import 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.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.database.PolicyDao 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.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskPolicy import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
import com.topjohnwu.magisk.model.events.PolicyEnableEvent import com.topjohnwu.magisk.model.events.PolicyEnableEvent
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
import com.topjohnwu.magisk.ui.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.FingerprintHelper
import com.topjohnwu.magisk.utils.RxBus
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
import io.reactivex.Single import io.reactivex.Single
@ -29,7 +29,7 @@ class SuperuserViewModel(
private val packageManager: PackageManager, private val packageManager: PackageManager,
private val resources: Resources, private val resources: Resources,
rxBus: RxBus rxBus: RxBus
) : MagiskViewModel() { ) : BaseViewModel() {
val items = DiffObservableList(ComparableRvItem.callback) val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item -> val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item ->

View File

@ -5,19 +5,20 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.Window import android.view.Window
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.databinding.ActivityRequestBinding import com.topjohnwu.magisk.databinding.ActivityRequestBinding
import com.topjohnwu.magisk.model.entity.MagiskPolicy import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.events.DieEvent 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.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.ui.base.MagiskActivity
import com.topjohnwu.magisk.utils.SuLogger import com.topjohnwu.magisk.utils.SuLogger
import org.koin.androidx.viewmodel.ext.android.viewModel 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 layoutRes: Int = R.layout.activity_request
override val themeRes: Int = R.style.MagiskTheme_SU
override val viewModel: SuRequestViewModel by viewModel() override val viewModel: SuRequestViewModel by viewModel()
override fun onBackPressed() { override fun onBackPressed() {

View File

@ -9,21 +9,21 @@ import android.graphics.drawable.Drawable
import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.FingerprintManager
import android.os.CountDownTimer import android.os.CountDownTimer
import android.text.TextUtils 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.BuildConfig
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.database.PolicyDao 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.extensions.now
import com.topjohnwu.magisk.model.entity.MagiskPolicy import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem
import com.topjohnwu.magisk.model.entity.toPolicy import com.topjohnwu.magisk.model.entity.toPolicy
import com.topjohnwu.magisk.model.events.DieEvent 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.FingerprintHelper
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.SuConnector import com.topjohnwu.magisk.utils.SuConnector
import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter
import me.tatarka.bindingcollectionadapter2.ItemBinding import me.tatarka.bindingcollectionadapter2.ItemBinding
@ -36,7 +36,7 @@ class SuRequestViewModel(
private val policyDB: PolicyDao, private val policyDB: PolicyDao,
private val timeoutPrefs: SharedPreferences, private val timeoutPrefs: SharedPreferences,
private val resources: Resources private val resources: Resources
) : MagiskViewModel() { ) : BaseViewModel() {
val icon = KObservableField<Drawable?>(null) val icon = KObservableField<Drawable?>(null)
val title = KObservableField("") val title = KObservableField("")

View File

@ -21,9 +21,9 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager import androidx.viewpager.widget.ViewPager
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.extensions.replaceRandomWithSpecial import com.topjohnwu.magisk.extensions.replaceRandomWithSpecial
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.state.IndeterminateState import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable

View File

@ -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)
}
}
}

View File

@ -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()
}
}
}
}

View 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)
}
}
}

View File

@ -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)"
}
}

View File

@ -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)
}
}

View File

@ -3,8 +3,8 @@ package com.topjohnwu.magisk.utils
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.widget.Toast import android.widget.Toast
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.* import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.ui.SplashActivity import com.topjohnwu.magisk.ui.SplashActivity
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.signing.JarMap import com.topjohnwu.signing.JarMap
@ -102,7 +102,7 @@ object PatchAPK {
Config.suManager = pkg Config.suManager = pkg
Config.export() Config.export()
RootUtils.rmAndLaunch(BuildConfig.APPLICATION_ID, Utils.rmAndLaunch(BuildConfig.APPLICATION_ID,
ComponentName(pkg, ClassMap.get<Class<*>>(SplashActivity::class.java).name)) ComponentName(pkg, ClassMap.get<Class<*>>(SplashActivity::class.java).name))
return true return true

View 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() }
}
}

View 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
}
}

View File

@ -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()
}
}
}

View 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
}

View File

@ -1,5 +1,6 @@
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.utils
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
@ -72,4 +73,8 @@ object Utils {
if ((exists() && isDirectory) || mkdirs()) this else null if ((exists() && isDirectory) || mkdirs()) this else null
} }
fun rmAndLaunch(rm: String, component: ComponentName) {
Shell.su("(rm_launch $rm ${component.flattenToString()})").exec()
}
} }

View File

@ -4,23 +4,22 @@ import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.repository.StringRepository import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.magisk.extensions.subscribeK
import io.noties.markwon.Markwon
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Single import io.reactivex.Single
import ru.noties.markwon.Markwon import org.koin.core.KoinComponent
import ru.noties.markwon.html.HtmlPlugin import org.koin.core.inject
import ru.noties.markwon.image.ImagesPlugin
import ru.noties.markwon.image.svg.SvgPlugin
import timber.log.Timber import timber.log.Timber
import java.io.InputStream import java.io.InputStream
import java.util.* import java.util.*
object MarkDownWindow { object MarkDownWindow : KoinComponent {
private val stringRepo: StringRepository by inject() private val stringRepo: StringRepository by inject()
private val markwon: Markwon by inject()
fun show(activity: Context, title: String?, url: String) { fun show(activity: Context, title: String?, url: String) {
show(activity, title, stringRepo.getString(url)) show(activity, title, stringRepo.getString(url))
@ -35,25 +34,14 @@ object MarkDownWindow {
} }
fun show(activity: Context, title: String?, content: Single<String>) { 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 mv = LayoutInflater.from(activity).inflate(R.layout.markdown_window, null)
val tv = mv.findViewById<TextView>(R.id.md_txt) val tv = mv.findViewById<TextView>(R.id.md_txt)
content.map { content.map {
runCatching {
markwon.setMarkdown(tv, it) 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)
}
}.ignoreElement().onErrorResumeNext { }.ignoreElement().onErrorResumeNext {
// Nothing we can actually do other than show error message // Nothing we can actually do other than show error message
Timber.e(it)
tv.setText(R.string.download_file_error) tv.setText(R.string.download_file_error)
Completable.complete() Completable.complete()
}.subscribeK { }.subscribeK {
@ -64,6 +52,4 @@ object MarkDownWindow {
.show() .show()
} }
} }
class MarkwonException(e: Throwable): Exception(e)
} }

View File

@ -8,9 +8,9 @@ import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.extensions.cachedFile import com.topjohnwu.magisk.extensions.cachedFile
import com.topjohnwu.magisk.extensions.reboot import com.topjohnwu.magisk.extensions.reboot
import com.topjohnwu.magisk.net.Networking
import com.topjohnwu.magisk.tasks.MagiskInstaller import com.topjohnwu.magisk.tasks.MagiskInstaller
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.net.Networking
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler

View File

@ -7,13 +7,13 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.model.download.DownloadService import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.internal.Configuration import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.ui.base.MagiskActivity
import com.topjohnwu.magisk.utils.Utils 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) { AlertDialog.Builder(activity) {
init { 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) subject = DownloadSubject.Magisk(Configuration.Flash.Primary)
} }
private fun patchBoot(activity: MagiskActivity<*, *>) = activity.withExternalRW { private fun patchBoot(activity: BaseActivity<*, *>) = activity.withExternalRW {
onSuccess { onSuccess {
Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG) Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
val intent = Intent(Intent.ACTION_GET_CONTENT) 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 { onSuccess {
DownloadService(activity) { DownloadService(activity) {
subject = DownloadSubject.Magisk(Configuration.Download) 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) CustomAlertDialog(activity)
.setTitle(R.string.warning) .setTitle(R.string.warning)
.setMessage(R.string.install_inactive_slot_msg) .setMessage(R.string.install_inactive_slot_msg)

View File

@ -3,14 +3,14 @@ package com.topjohnwu.magisk.view.dialogs
import android.net.Uri import android.net.Uri
import com.topjohnwu.magisk.Info import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R 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.utils.Utils
import com.topjohnwu.magisk.view.MarkDownWindow import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
import java.util.* import java.util.*
class MagiskInstallDialog(a: MagiskActivity<*, *>) : CustomAlertDialog(a) { class MagiskInstallDialog(a: BaseActivity<*, *>) : CustomAlertDialog(a) {
init { init {
val filename = "Magisk v${Info.remote.magisk.version}" + val filename = "Magisk v${Info.remote.magisk.version}" +
"(${Info.remote.magisk.versionCode})" "(${Info.remote.magisk.versionCode})"

View File

@ -5,7 +5,7 @@
<data> <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" /> <import type="com.topjohnwu.magisk.ui.home.SafetyNetState" />

View File

@ -1,4 +1,4 @@
# v7.3.4 # v7.3.5
- App is now fully written in Kotlin! - Sort installed modules by name
- New downloading system - Better pre-5.0 support
- Add new "Recovery Mode" to Advanced Settings - Fix potential issues when patching tar files

Some files were not shown because too many files have changed in this diff Show More