Merge branch 'md2'
This commit is contained in:
commit
c1dad11cb3
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -17,3 +17,4 @@ tools/** binary
|
||||
*.apk binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ttf binary
|
||||
|
@ -19,6 +19,12 @@ android {
|
||||
multiDexEnabled true
|
||||
versionName props['appVersion']
|
||||
versionCode props['appVersionCode'] as Integer
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = ["room.incremental":"true"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -62,7 +68,7 @@ dependencies {
|
||||
implementation 'com.ncapdevi:frag-nav:3.2.0'
|
||||
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.13'
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.16'
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
|
||||
@ -89,14 +95,16 @@ dependencies {
|
||||
implementation "org.koin:koin-android:${vKoin}"
|
||||
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
|
||||
|
||||
def vRetrofit = '2.6.2'
|
||||
def vRetrofit = '2.7.1'
|
||||
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
|
||||
|
||||
def vOkHttp = '3.12.6'
|
||||
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
|
||||
def vOkHttp = '3.12.7'
|
||||
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}") {
|
||||
force = true
|
||||
}
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
|
||||
|
||||
def vMoshi = '1.9.2'
|
||||
@ -111,7 +119,7 @@ dependencies {
|
||||
replacedBy('com.github.topjohnwu:room-runtime')
|
||||
}
|
||||
}
|
||||
def vRoom = '2.2.2'
|
||||
def vRoom = '2.2.3'
|
||||
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
|
||||
implementation "androidx.room:room-rxjava2:${vRoom}"
|
||||
kapt "androidx.room:room-compiler:${vRoom}"
|
||||
@ -120,16 +128,16 @@ dependencies {
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:${vNav}"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:${vNav}"
|
||||
|
||||
implementation 'androidx.biometric:biometric:1.0.0'
|
||||
implementation 'androidx.biometric:biometric:1.0.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
|
||||
implementation 'androidx.browser:browser:1.2.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.0-rc03'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.0-rc05'
|
||||
implementation 'androidx.work:work-runtime:2.2.0'
|
||||
implementation 'androidx.transition:transition:1.3.0-rc02'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.core:core-ktx:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.2.0-alpha02'
|
||||
implementation 'androidx.core:core-ktx:1.2.0-rc01'
|
||||
implementation 'com.google.android.material:material:1.2.0-alpha03'
|
||||
}
|
||||
|
22
app/proguard-rules.pro
vendored
22
app/proguard-rules.pro
vendored
@ -17,32 +17,26 @@
|
||||
#}
|
||||
|
||||
# Snet
|
||||
-keepclassmembers class com.topjohnwu.magisk.utils.SafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.SafetyNetHelper$Callback
|
||||
-keepclassmembers class * implements com.topjohnwu.magisk.utils.SafetyNetHelper$Callback {
|
||||
-keepclassmembers class com.topjohnwu.magisk.core.utils.SafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.core.utils.SafetyNetHelper$Callback
|
||||
-keepclassmembers class * implements com.topjohnwu.magisk.core.utils.SafetyNetHelper$Callback {
|
||||
void onResponse(int);
|
||||
}
|
||||
|
||||
# Keep all fragment constructors
|
||||
-keepclassmembers class * extends androidx.fragment.app.Fragment {
|
||||
public <init>(...);
|
||||
}
|
||||
# Fragments
|
||||
-keep,allowobfuscation class * extends androidx.fragment.app.Fragment
|
||||
|
||||
# DelegateWorker
|
||||
-keep,allowobfuscation class * extends com.topjohnwu.magisk.base.DelegateWorker
|
||||
# BaseWorkerWrapper
|
||||
-keep,allowobfuscation class * extends com.topjohnwu.magisk.core.base.BaseWorkerWrapper
|
||||
|
||||
# BootSigner
|
||||
-keep class a.a { *; }
|
||||
|
||||
# Workaround R8 bug
|
||||
-keep,allowobfuscation class com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||
-keepclassmembers class a.e { *; }
|
||||
|
||||
# Strip logging
|
||||
-assumenosideeffects class timber.log.Timber.Tree { *; }
|
||||
|
||||
# Excessive obfuscation
|
||||
-repackageclasses 'a'
|
||||
-repackageclasses a
|
||||
-allowaccessmodification
|
||||
|
||||
# QOL
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.topjohnwu.magisk">
|
||||
@ -21,6 +20,10 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Main -->
|
||||
|
@ -1,6 +1,6 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.utils.PatchAPK;
|
||||
import com.topjohnwu.magisk.core.utils.PatchAPK;
|
||||
import com.topjohnwu.signing.BootSigner;
|
||||
|
||||
public class a {
|
||||
|
@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.MainActivity;
|
||||
|
||||
public class b extends MainActivity {
|
||||
/* stub */
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.SplashActivity;
|
||||
|
||||
public class c extends SplashActivity {
|
||||
/* stub */
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
|
||||
public class e extends App {
|
||||
public e() {
|
||||
super();
|
||||
}
|
||||
|
||||
public e(Object o) {
|
||||
super(o);
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity;
|
||||
|
||||
public class f extends FlashActivity {
|
||||
/* stub */
|
||||
}
|
@ -2,11 +2,11 @@ package a;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import com.topjohnwu.magisk.core.UpdateCheckService;
|
||||
|
||||
public class g extends w<UpdateCheckService> {
|
||||
/* Stub */
|
||||
public g(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
|
@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver;
|
||||
|
||||
public class h extends GeneralReceiver {
|
||||
/* stub */
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.model.download.DownloadService;
|
||||
|
||||
public class j extends DownloadService {
|
||||
/* stub */
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity;
|
||||
|
||||
public class m extends SuRequestActivity {
|
||||
/* stub */
|
||||
}
|
55
app/src/main/java/a/stubs.kt
Normal file
55
app/src/main/java/a/stubs.kt
Normal file
@ -0,0 +1,55 @@
|
||||
package a
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.topjohnwu.magisk.core.App
|
||||
import com.topjohnwu.magisk.core.GeneralReceiver
|
||||
import com.topjohnwu.magisk.core.SplashActivity
|
||||
import com.topjohnwu.magisk.core.base.BaseWorkerWrapper
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.legacy.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.legacy.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import java.lang.reflect.ParameterizedType
|
||||
|
||||
class b : MainActivity()
|
||||
|
||||
class c : SplashActivity()
|
||||
|
||||
class e : App {
|
||||
constructor() : super()
|
||||
constructor(o: Any) : super(o)
|
||||
}
|
||||
|
||||
class f : FlashActivity()
|
||||
|
||||
class h : GeneralReceiver()
|
||||
|
||||
class j : DownloadService()
|
||||
|
||||
class m : SuRequestActivity()
|
||||
|
||||
/**
|
||||
* Wrapper class to workaround Proguard rule :
|
||||
* -keep class * extends Worker
|
||||
* */
|
||||
abstract class w<T : BaseWorkerWrapper>(
|
||||
context: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : Worker(context, workerParams) {
|
||||
|
||||
private var base: T? = null
|
||||
|
||||
override fun doWork() = base?.doWork() ?: Result.failure()
|
||||
|
||||
override fun onStopped() = base?.onStopped() ?: Unit
|
||||
|
||||
init {
|
||||
try {
|
||||
base = ((javaClass.genericSuperclass as ParameterizedType)
|
||||
.actualTypeArguments[0] as Class<T>).newInstance()
|
||||
base?.attachWorker(this)
|
||||
} catch (e : java.lang.Exception) {}
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package a;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import com.topjohnwu.magisk.base.DelegateWorker;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
||||
public abstract class w<T extends DelegateWorker> extends Worker {
|
||||
|
||||
/* Wrapper class to workaround Proguard -keep class * extends Worker */
|
||||
|
||||
private T base;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
w(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
try {
|
||||
base = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass())
|
||||
.getActualTypeArguments()[0]).newInstance();
|
||||
base.attachWorker(this);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
if (base == null)
|
||||
return Result.failure();
|
||||
return base.doWork();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopped() {
|
||||
if (base != null)
|
||||
base.onStopped();
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.preference.*
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
abstract class BasePreferenceFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
protected val prefs: SharedPreferences by inject()
|
||||
protected val activity get() = requireActivity() as BaseActivity<*, *>
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val v = super.onCreateView(inflater, container, savedInstanceState)
|
||||
prefs.registerOnSharedPreferenceChangeListener(this)
|
||||
return v
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setAllPreferencesToAvoidHavingExtraSpace(preference: Preference) {
|
||||
preference.isIconSpaceReserved = false
|
||||
if (preference is PreferenceGroup)
|
||||
for (i in 0 until preference.preferenceCount)
|
||||
setAllPreferencesToAvoidHavingExtraSpace(preference.getPreference(i))
|
||||
}
|
||||
|
||||
override fun setPreferenceScreen(preferenceScreen: PreferenceScreen?) {
|
||||
if (preferenceScreen != null)
|
||||
setAllPreferencesToAvoidHavingExtraSpace(preferenceScreen)
|
||||
super.setPreferenceScreen(preferenceScreen)
|
||||
}
|
||||
|
||||
override fun onCreateAdapter(preferenceScreen: PreferenceScreen?): RecyclerView.Adapter<*> =
|
||||
object : PreferenceGroupAdapter(preferenceScreen) {
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onPreferenceHierarchyChange(preference: Preference?) {
|
||||
if (preference != null)
|
||||
setAllPreferencesToAvoidHavingExtraSpace(preference)
|
||||
super.onPreferenceHierarchyChange(preference)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import com.topjohnwu.magisk.base.BaseActivity
|
||||
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
|
||||
import com.topjohnwu.magisk.model.events.BackPressEvent
|
||||
import com.topjohnwu.magisk.model.events.PermissionEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewActionEvent
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import com.topjohnwu.magisk.Info.isConnected as gIsConnected
|
||||
|
||||
|
||||
abstract class BaseViewModel(
|
||||
initialState: State = State.LOADING
|
||||
) : LoadingViewModel(initialState) {
|
||||
|
||||
val isConnected = object : KObservableField<Boolean>(gIsConnected.value, gIsConnected) {
|
||||
override fun get(): Boolean {
|
||||
return gIsConnected.value
|
||||
}
|
||||
}
|
||||
|
||||
fun withView(action: BaseActivity<*, *>.() -> Unit) {
|
||||
ViewActionEvent(action).publish()
|
||||
}
|
||||
|
||||
fun withPermissions(vararg permissions: String): Observable<Boolean> {
|
||||
val subject = PublishSubject.create<Boolean>()
|
||||
return subject.doOnSubscribeUi { PermissionEvent(permissions.toList(), subject).publish() }
|
||||
}
|
||||
|
||||
fun back() = BackPressEvent().publish()
|
||||
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import io.reactivex.*
|
||||
|
||||
abstract class LoadingViewModel(defaultState: State = State.LOADING) :
|
||||
StatefulViewModel<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
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
/**
|
||||
* Copy of [android.databinding.BaseObservable] which extends [ViewModel]
|
||||
*/
|
||||
abstract class ObservableViewModel : TeanityViewModel(), Observable {
|
||||
|
||||
@Transient
|
||||
private var callbacks: PropertyChangeRegistry? = null
|
||||
|
||||
@Synchronized
|
||||
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
|
||||
if (callbacks == null) {
|
||||
callbacks = PropertyChangeRegistry()
|
||||
}
|
||||
callbacks?.add(callback)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
|
||||
callbacks?.remove(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies listeners that all properties of this instance have changed.
|
||||
*/
|
||||
@Synchronized
|
||||
fun notifyChange() {
|
||||
callbacks?.notifyCallbacks(this, 0, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies listeners that a specific property has changed. The getter for the property
|
||||
* that changes should be marked with [android.databinding.Bindable] to generate a field in
|
||||
* `BR` to be used as `fieldId`.
|
||||
*
|
||||
* @param fieldId The generated BR id for the Bindable field.
|
||||
*/
|
||||
fun notifyPropertyChanged(fieldId: Int) {
|
||||
callbacks?.notifyCallbacks(this, fieldId, null)
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
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
|
||||
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.topjohnwu.magisk.model.events.SimpleViewEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
abstract class TeanityViewModel : ViewModel() {
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
private val _viewEvents = MutableLiveData<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)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
@ -9,6 +9,12 @@ import androidx.room.Room
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.impl.WorkDatabase
|
||||
import androidx.work.impl.WorkDatabase_Impl
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.FileProvider
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import com.topjohnwu.magisk.core.utils.RootInit
|
||||
import com.topjohnwu.magisk.core.utils.updateConfig
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase_Impl
|
||||
import com.topjohnwu.magisk.data.database.SuLogDatabase
|
||||
@ -17,13 +23,11 @@ import com.topjohnwu.magisk.di.ActivityTracker
|
||||
import com.topjohnwu.magisk.di.koinModules
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.unwrap
|
||||
import com.topjohnwu.magisk.utils.RootInit
|
||||
import com.topjohnwu.magisk.utils.SuHandler
|
||||
import com.topjohnwu.magisk.utils.updateConfig
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
import timber.log.Timber
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
open class App() : Application() {
|
||||
|
||||
@ -37,7 +41,7 @@ open class App() : Application() {
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||
Shell.Config.addInitializers(RootInit::class.java)
|
||||
Shell.Config.setTimeout(2)
|
||||
FileProvider.callHandler = SuHandler
|
||||
FileProvider.callHandler = SuCallbackHandler
|
||||
Room.setFactory {
|
||||
when (it) {
|
||||
WorkDatabase::class.java -> WorkDatabase_Impl()
|
||||
@ -46,13 +50,19 @@ open class App() : Application() {
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
// Always log full stack trace with Timber
|
||||
Timber.plant(Timber.DebugTree())
|
||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||
Timber.e(e)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
// Basic setup
|
||||
if (BuildConfig.DEBUG)
|
||||
MultiDex.install(base)
|
||||
Timber.plant(Timber.DebugTree())
|
||||
|
||||
// Some context magic
|
||||
val app: Application
|
@ -1,19 +1,22 @@
|
||||
package com.topjohnwu.magisk
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.util.Xml
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.model.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
@ -45,10 +48,15 @@ object Config : PreferenceModel, DBConfig {
|
||||
const val CUSTOM_CHANNEL = "custom_channel"
|
||||
const val LOCALE = "locale"
|
||||
const val DARK_THEME = "dark_theme"
|
||||
const val DARK_THEME_EXTENDED = "dark_theme_extended"
|
||||
const val REPO_ORDER = "repo_order"
|
||||
const val SHOW_SYSTEM_APP = "show_system"
|
||||
const val DOWNLOAD_PATH = "download_path"
|
||||
const val REDESIGN = "redesign"
|
||||
const val SAFETY = "safety_notice"
|
||||
const val THEME_ORDINAL = "theme_ordinal"
|
||||
const val BOOT_ID = "boot_id"
|
||||
const val LIST_SPAN_COUNT = "list_span_count"
|
||||
|
||||
// system state
|
||||
const val MAGISKHIDE = "magiskhide"
|
||||
@ -103,39 +111,67 @@ object Config : PreferenceModel, DBConfig {
|
||||
Value.CANARY_DEBUG_CHANNEL
|
||||
else
|
||||
Value.CANARY_CHANNEL
|
||||
}
|
||||
else Value.DEFAULT_CHANNEL
|
||||
} else Value.DEFAULT_CHANNEL
|
||||
|
||||
var bootId by preference(Key.BOOT_ID, "")
|
||||
|
||||
var downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
|
||||
var repoOrder by preference(
|
||||
Key.REPO_ORDER,
|
||||
Value.ORDER_DATE
|
||||
)
|
||||
|
||||
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
|
||||
var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
|
||||
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
|
||||
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
|
||||
var suAutoReponse by preferenceStrInt(
|
||||
Key.SU_AUTO_RESPONSE,
|
||||
Value.SU_PROMPT
|
||||
)
|
||||
var suNotification by preferenceStrInt(
|
||||
Key.SU_NOTIFICATION,
|
||||
Value.NOTIFICATION_TOAST
|
||||
)
|
||||
var updateChannel by preferenceStrInt(
|
||||
Key.UPDATE_CHANNEL,
|
||||
defaultChannel
|
||||
)
|
||||
|
||||
var darkTheme by preference(Key.DARK_THEME, true)
|
||||
var safetyNotice by preference(Key.SAFETY, true)
|
||||
var darkThemeExtended by preference(
|
||||
Key.DARK_THEME_EXTENDED,
|
||||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
)
|
||||
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||
var checkUpdate by preference(Key.CHECK_UPDATES, true)
|
||||
var magiskHide by preference(Key.MAGISKHIDE, true)
|
||||
@JvmStatic
|
||||
var coreOnly by preference(Key.COREONLY, false)
|
||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||
var listSpanCount by preference(Key.LIST_SPAN_COUNT, 2)
|
||||
|
||||
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
|
||||
var locale by preference(Key.LOCALE, "")
|
||||
|
||||
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
||||
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||
var rootMode by dbSettings(
|
||||
Key.ROOT_ACCESS,
|
||||
Value.ROOT_ACCESS_APPS_AND_ADB
|
||||
)
|
||||
var suMntNamespaceMode by dbSettings(
|
||||
Key.SU_MNT_NS,
|
||||
Value.NAMESPACE_MODE_REQUESTER
|
||||
)
|
||||
var suMultiuserMode by dbSettings(
|
||||
Key.SU_MULTIUSER_MODE,
|
||||
Value.MULTIUSER_MODE_OWNER_ONLY
|
||||
)
|
||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||
|
||||
// Always return a path in external storage where we can write
|
||||
val downloadDirectory get() =
|
||||
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
|
||||
val downloadDirectory
|
||||
get() =
|
||||
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
|
||||
|
||||
private const val SU_FINGERPRINT = "su_fingerprint"
|
||||
|
||||
@ -163,7 +199,9 @@ object Config : PreferenceModel, DBConfig {
|
||||
}
|
||||
|
||||
private fun parsePrefs(editor: SharedPreferences.Editor) = editor.apply {
|
||||
val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS)
|
||||
val config = SuFile.open("/data/adb",
|
||||
Const.MANAGER_CONFIGS
|
||||
)
|
||||
if (config.exists()) runCatching {
|
||||
val input = SuFileInputStream(config)
|
||||
val parser = Xml.newPullParser()
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.os.Process
|
||||
import java.io.File
|
||||
@ -62,6 +62,7 @@ object Const {
|
||||
const val ETAG_KEY = "ETag"
|
||||
// intents
|
||||
const val OPEN_SECTION = "section"
|
||||
const val OPEN_SETTINGS = "settings"
|
||||
const val INTENT_SET_APP = "app_json"
|
||||
const val FLASH_ACTION = "action"
|
||||
const val FLASH_DATA = "additional_data"
|
@ -1,19 +1,16 @@
|
||||
package com.topjohnwu.magisk.model.receiver
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.base.BaseReceiver
|
||||
import com.topjohnwu.magisk.data.database.PolicyDao
|
||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.model.ManagerJson
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import com.topjohnwu.magisk.core.view.Shortcuts
|
||||
import com.topjohnwu.magisk.extensions.reboot
|
||||
import com.topjohnwu.magisk.model.download.DownloadService
|
||||
import com.topjohnwu.magisk.model.entity.ManagerJson
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.utils.SuHandler
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.core.inject
|
||||
|
||||
@ -30,7 +27,7 @@ open class GeneralReceiver : BaseReceiver() {
|
||||
|
||||
when (intent.action ?: return) {
|
||||
Intent.ACTION_REBOOT -> {
|
||||
SuHandler(context, intent.getStringExtra("action"), intent.extras)
|
||||
SuCallbackHandler(context, intent.getStringExtra("action"), intent.extras)
|
||||
}
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
// This will only work pre-O
|
@ -1,6 +1,6 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.job.JobInfo
|
||||
@ -14,23 +14,22 @@ import android.content.res.AssetManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.ProcessPhoenix
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.core.utils.updateConfig
|
||||
import com.topjohnwu.magisk.extensions.forceGetDeclaredField
|
||||
import com.topjohnwu.magisk.model.download.DownloadService
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService
|
||||
import com.topjohnwu.magisk.legacy.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.legacy.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.ui.SplashActivity
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.utils.updateConfig
|
||||
|
||||
fun AssetManager.addAssetPath(path: String) {
|
||||
DynAPK.addAssetPath(this, path)
|
||||
}
|
||||
|
||||
fun Context.wrap(global: Boolean = true): Context
|
||||
= if (global) GlobalResContext(this) else ResContext(this)
|
||||
fun Context.wrap(global: Boolean = true): Context =
|
||||
if (global) GlobalResContext(this) else ResContext(this)
|
||||
|
||||
fun Context.wrapJob(): Context = object : GlobalResContext(this) {
|
||||
|
||||
@ -130,7 +129,8 @@ private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler
|
||||
val name = service.className
|
||||
val component = ComponentName(
|
||||
service.packageName,
|
||||
Info.stub!!.classToComponent[name] ?: name)
|
||||
Info.stub!!.classToComponent[name] ?: name
|
||||
)
|
||||
|
||||
javaClass.forceGetDeclaredField("service")?.set(this, component)
|
||||
return this
|
@ -1,9 +1,10 @@
|
||||
package com.topjohnwu.magisk
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
||||
import com.topjohnwu.magisk.utils.CachedValue
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import com.topjohnwu.superuser.Shell
|
||||
@ -17,12 +18,16 @@ object Info {
|
||||
|
||||
val envRef = CachedValue { loadState() }
|
||||
|
||||
@JvmStatic
|
||||
val env by envRef // Local
|
||||
var remote = UpdateInfo() // Remote
|
||||
var stub: DynAPK.Data? = null // Stub
|
||||
|
||||
@JvmStatic
|
||||
var keepVerity = false
|
||||
@JvmStatic
|
||||
var keepEnc = false
|
||||
@JvmStatic
|
||||
var recovery = false
|
||||
|
||||
val isConnected by lazy {
|
@ -1,12 +1,13 @@
|
||||
package com.topjohnwu.magisk.ui
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.model.navigation.Navigation
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.magisk.core.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
|
||||
@ -61,8 +62,7 @@ open class SplashActivity : Activity() {
|
||||
}
|
||||
|
||||
DONE = true
|
||||
|
||||
startActivity(intent<MainActivity>().apply { intent?.also { putExtras(it) } })
|
||||
Navigation.start(intent, this)
|
||||
finish()
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
package com.topjohnwu.magisk.model.update
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import androidx.work.ListenableWorker
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.base.DelegateWorker
|
||||
import com.topjohnwu.magisk.core.base.BaseWorkerWrapper
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
class UpdateCheckService : DelegateWorker() {
|
||||
class UpdateCheckService : BaseWorkerWrapper() {
|
||||
|
||||
private val magiskRepo: MagiskRepository by inject()
|
||||
|
@ -1,51 +1,26 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.extensions.set
|
||||
import com.topjohnwu.magisk.model.events.EventHandler
|
||||
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import com.topjohnwu.magisk.wrap
|
||||
import kotlin.random.Random
|
||||
|
||||
typealias RequestCallback = BaseActivity<*, *>.(Int, Intent?) -> Unit
|
||||
typealias RequestCallback = BaseActivity.(Int, Intent?) -> Unit
|
||||
|
||||
abstract class BaseActivity<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
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
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)
|
||||
@ -56,18 +31,6 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding
|
||||
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 {
|
||||
@ -93,7 +56,7 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
var success = true
|
||||
for (res in grantResults) {
|
||||
if (res != PackageManager.PERMISSION_GRANTED) {
|
||||
@ -101,18 +64,18 @@ abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding
|
||||
break
|
||||
}
|
||||
}
|
||||
resultCallbacks[requestCode]?.apply {
|
||||
resultCallbacks[requestCode]?.also {
|
||||
resultCallbacks.remove(requestCode)
|
||||
invoke(this@BaseActivity, if (success) 1 else -1, null)
|
||||
it(this@BaseActivity, if (success) 1 else -1, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
resultCallbacks[requestCode]?.apply {
|
||||
resultCallbacks[requestCode]?.also {
|
||||
resultCallbacks.remove(requestCode)
|
||||
invoke(this@BaseActivity, resultCode, data)
|
||||
it(this@BaseActivity, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.wrap
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import org.koin.core.KoinComponent
|
||||
|
||||
abstract class BaseReceiver : BroadcastReceiver(), KoinComponent {
|
@ -1,8 +1,8 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.wrap
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import org.koin.core.KoinComponent
|
||||
|
||||
abstract class BaseService : Service(), KoinComponent {
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Network
|
||||
@ -10,7 +10,7 @@ import androidx.work.ListenableWorker
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.util.*
|
||||
|
||||
abstract class DelegateWorker {
|
||||
abstract class BaseWorkerWrapper {
|
||||
|
||||
private lateinit var worker: ListenableWorker
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
@ -8,15 +8,15 @@ import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.extensions.chooser
|
||||
import com.topjohnwu.magisk.extensions.exists
|
||||
import com.topjohnwu.magisk.extensions.provide
|
||||
import com.topjohnwu.magisk.intent
|
||||
import com.topjohnwu.magisk.legacy.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import org.koin.core.get
|
||||
import java.io.File
|
||||
@ -114,13 +114,15 @@ open class DownloadService : RemoteFileService() {
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun Notification.Builder.setContentIntent(intent: Intent) =
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
.let { setContentIntent(it) }
|
||||
setContentIntent(
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
)
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun Notification.Builder.addAction(icon: Int, title: Int, intent: Intent) =
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
.let { addAction(icon, getString(title), it) }
|
||||
addAction(icon, getString(title),
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
)
|
||||
|
||||
// ---
|
||||
|
@ -1,11 +1,18 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.ProcessPhoenix
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.utils.PatchAPK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Restore
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Upgrade
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.utils.PatchAPK
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.io.File
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import com.topjohnwu.magisk.extensions.withStreams
|
||||
import java.io.File
|
||||
@ -41,4 +41,4 @@ fun InputStream.toModule(file: File, installer: InputStream) {
|
||||
entry = zin.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.magisk.base.BaseService
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.core.base.BaseService
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import org.koin.core.KoinComponent
|
||||
import java.util.*
|
||||
import kotlin.random.Random.Default.nextInt
|
@ -1,8 +1,10 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.di.NullActivity
|
||||
@ -11,12 +13,13 @@ import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
||||
import com.topjohnwu.magisk.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import io.reactivex.Completable
|
||||
import okhttp3.ResponseBody
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.KoinComponent
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
|
||||
@ -39,11 +42,7 @@ abstract class RemoteFileService : NotificationService() {
|
||||
.doOnSubscribe { update(subject.hashCode()) { it.setContentTitle(subject.title) } }
|
||||
.subscribeK(onError = {
|
||||
Timber.e(it)
|
||||
finishNotify(subject.hashCode()) { notification ->
|
||||
notification.setContentText(getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
failNotify(subject)
|
||||
}) {
|
||||
val newId = finishNotify(subject)
|
||||
if (get<Activity>() !is NullActivity) {
|
||||
@ -62,12 +61,12 @@ abstract class RemoteFileService : NotificationService() {
|
||||
}
|
||||
|
||||
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
|
||||
.map { it.toStream(subject.hashCode()) }
|
||||
.map { it.toStream(subject.hashCode(), subject) }
|
||||
.flatMapCompletable { stream ->
|
||||
when (subject) {
|
||||
is Module -> service.fetchInstaller()
|
||||
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
|
||||
.ignoreElement()
|
||||
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
|
||||
.ignoreElement()
|
||||
else -> Completable.fromAction { stream.writeTo(subject.file) }
|
||||
}
|
||||
}.doOnComplete {
|
||||
@ -75,7 +74,7 @@ abstract class RemoteFileService : NotificationService() {
|
||||
handleAPK(subject)
|
||||
}
|
||||
|
||||
private fun ResponseBody.toStream(id: Int): InputStream {
|
||||
private fun ResponseBody.toStream(id: Int, subject: DownloadSubject): InputStream {
|
||||
val maxRaw = contentLength()
|
||||
val max = maxRaw / 1_000_000f
|
||||
|
||||
@ -83,17 +82,27 @@ abstract class RemoteFileService : NotificationService() {
|
||||
val progress = it / 1_000_000f
|
||||
update(id) { notification ->
|
||||
if (maxRaw > 0) {
|
||||
send(progress / max, subject)
|
||||
notification
|
||||
.setProgress(maxRaw.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, max))
|
||||
.setProgress(maxRaw.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, max))
|
||||
} else {
|
||||
send(-1f, subject)
|
||||
notification.setContentText("%.2f MB / ??".format(progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun failNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) {
|
||||
send(0f, subject)
|
||||
it.setContentText(getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
|
||||
private fun finishNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) {
|
||||
send(1f, subject)
|
||||
it.addActions(subject)
|
||||
.setContentText(getString(R.string.download_complete))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
@ -111,8 +120,19 @@ abstract class RemoteFileService : NotificationService() {
|
||||
protected abstract fun Notification.Builder.addActions(subject: DownloadSubject)
|
||||
: Notification.Builder
|
||||
|
||||
companion object {
|
||||
companion object : KoinComponent {
|
||||
const val ARG_URL = "arg_url"
|
||||
|
||||
private val internalProgressBroadcast = MutableLiveData<Pair<Float, DownloadSubject>>()
|
||||
val progressBroadcast: LiveData<Pair<Float, DownloadSubject>> get() = internalProgressBroadcast
|
||||
|
||||
fun send(progress: Float, subject: DownloadSubject) {
|
||||
internalProgressBroadcast.postValue(progress to subject)
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
internalProgressBroadcast.value = null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.data.database.magiskdb
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
import com.topjohnwu.superuser.Shell
|
@ -1,16 +1,12 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.BaseDao
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Delete
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Replace
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Select
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy
|
||||
import com.topjohnwu.magisk.core.model.toMap
|
||||
import com.topjohnwu.magisk.core.model.toPolicy
|
||||
import com.topjohnwu.magisk.extensions.now
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.toMap
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.data.database.magiskdb
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
|
@ -1,9 +1,4 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.BaseDao
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Delete
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Replace
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Select
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
class SettingsDao : BaseDao() {
|
||||
|
@ -1,9 +1,4 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.BaseDao
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Delete
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Replace
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Select
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
class StringDao : BaseDao() {
|
||||
|
@ -1,9 +1,9 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
package com.topjohnwu.magisk.core.model
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.INTERACTIVE
|
||||
import com.topjohnwu.magisk.extensions.getLabel
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE
|
||||
|
||||
|
||||
data class MagiskPolicy(
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
package com.topjohnwu.magisk.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
abstract class BaseModule : Comparable<BaseModule> {
|
||||
abstract var id: String
|
@ -1,7 +1,7 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
|
@ -1,9 +1,9 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.legalFilename
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@ -6,20 +6,26 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.ProviderCallHandler
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy
|
||||
import com.topjohnwu.magisk.core.model.toPolicy
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.startActivity
|
||||
import com.topjohnwu.magisk.extensions.startActivityWithRoot
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.legacy.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
|
||||
object SuHandler : ProviderCallHandler {
|
||||
object SuCallbackHandler : ProviderCallHandler {
|
||||
|
||||
const val REQUEST = "request"
|
||||
const val LOG = "log"
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
@ -0,0 +1,103 @@
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.CountDownTimer
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy
|
||||
import com.topjohnwu.magisk.core.model.toPolicy
|
||||
import com.topjohnwu.magisk.extensions.now
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class SuRequestHandler(
|
||||
private val packageManager: PackageManager,
|
||||
private val policyDB: PolicyDao
|
||||
) {
|
||||
protected var timer: CountDownTimer = object : CountDownTimer(
|
||||
TimeUnit.MINUTES.toMillis(1), TimeUnit.MINUTES.toMillis(1)) {
|
||||
override fun onFinish() {
|
||||
respond(MagiskPolicy.DENY, 0)
|
||||
}
|
||||
override fun onTick(remains: Long) {}
|
||||
}
|
||||
set(value) {
|
||||
field.cancel()
|
||||
field = value
|
||||
field.start()
|
||||
}
|
||||
|
||||
protected lateinit var policy: MagiskPolicy
|
||||
|
||||
private val cleanupTasks = mutableListOf<() -> Unit>()
|
||||
private lateinit var connector: SuConnector
|
||||
|
||||
abstract fun onStart()
|
||||
abstract fun onRespond()
|
||||
|
||||
fun start(intent: Intent): Boolean {
|
||||
val socketName = intent.getStringExtra("socket") ?: return false
|
||||
|
||||
try {
|
||||
connector = object : SuConnector(socketName) {
|
||||
override fun onResponse() {
|
||||
out.writeInt(policy.policy)
|
||||
}
|
||||
}
|
||||
val map = connector.readRequest()
|
||||
val uid = map["uid"]?.toIntOrNull() ?: return false
|
||||
policy = uid.toPolicy(packageManager)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
// Never allow com.topjohnwu.magisk (could be malware)
|
||||
if (policy.packageName == BuildConfig.APPLICATION_ID)
|
||||
return false
|
||||
|
||||
when (Config.suAutoReponse) {
|
||||
Config.Value.SU_AUTO_DENY -> {
|
||||
respond(MagiskPolicy.DENY, 0)
|
||||
return true
|
||||
}
|
||||
Config.Value.SU_AUTO_ALLOW -> {
|
||||
respond(MagiskPolicy.ALLOW, 0)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
timer.start()
|
||||
cleanupTasks.add {
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
onStart()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun respond() {
|
||||
connector.response()
|
||||
cleanupTasks.forEach { it() }
|
||||
onRespond()
|
||||
}
|
||||
|
||||
fun respond(action: Int, time: Int) {
|
||||
val until = if (time > 0)
|
||||
TimeUnit.MILLISECONDS.toSeconds(now) + TimeUnit.MINUTES.toSeconds(time.toLong())
|
||||
else
|
||||
time.toLong()
|
||||
|
||||
policy.policy = action
|
||||
policy.until = until
|
||||
policy.uid = policy.uid % 100000 + Const.USER_ID * 100000
|
||||
|
||||
if (until >= 0)
|
||||
policyDB.update(policy).blockingAwait()
|
||||
|
||||
respond()
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
package com.topjohnwu.magisk.tasks
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.extensions.fileName
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.readUri
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.utils.unzip
|
||||
import com.topjohnwu.magisk.core.utils.unzip
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
import java.io.File
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.tasks
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
@ -6,11 +6,13 @@ import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Info
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.extensions.*
|
||||
import com.topjohnwu.magisk.net.Networking
|
||||
import com.topjohnwu.signing.SignBoot
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
@ -34,10 +36,10 @@ import java.util.zip.ZipInputStream
|
||||
|
||||
abstract class MagiskInstaller {
|
||||
|
||||
protected lateinit var srcBoot: String
|
||||
protected lateinit var destFile: File
|
||||
protected lateinit var installDir: File
|
||||
protected lateinit var zipUri: Uri
|
||||
private lateinit var srcBoot: String
|
||||
private lateinit var destFile: File
|
||||
private lateinit var zipUri: Uri
|
||||
|
||||
private val console: MutableList<String>
|
||||
private val logs: MutableList<String>
|
||||
@ -60,7 +62,7 @@ abstract class MagiskInstaller {
|
||||
installDir.mkdirs()
|
||||
}
|
||||
|
||||
protected fun findImage(): Boolean {
|
||||
private fun findImage(): Boolean {
|
||||
srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||
if (srcBoot.isEmpty()) {
|
||||
console.add("! Unable to detect target image")
|
||||
@ -70,7 +72,7 @@ abstract class MagiskInstaller {
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun findSecondaryImage(): Boolean {
|
||||
private fun findSecondaryImage(): Boolean {
|
||||
val slot = "echo \$SLOT".fsh()
|
||||
val target = if (slot == "_a") "_b" else "_a"
|
||||
console.add("- Target slot: $target")
|
||||
@ -87,7 +89,7 @@ abstract class MagiskInstaller {
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun extractZip(): Boolean {
|
||||
private fun extractZip(): Boolean {
|
||||
val arch: String
|
||||
arch = if (Build.VERSION.SDK_INT >= 21) {
|
||||
val abis = listOf(*Build.SUPPORTED_ABIS)
|
||||
@ -208,7 +210,7 @@ abstract class MagiskInstaller {
|
||||
}
|
||||
}
|
||||
|
||||
protected fun handleFile(uri: Uri): Boolean {
|
||||
private fun handleFile(uri: Uri): Boolean {
|
||||
try {
|
||||
context.readUri(uri).buffered().use {
|
||||
it.mark(500)
|
||||
@ -238,7 +240,7 @@ abstract class MagiskInstaller {
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun patchBoot(): Boolean {
|
||||
private fun patchBoot(): Boolean {
|
||||
var isSigned = false
|
||||
try {
|
||||
SuFileInputStream(srcBoot).use {
|
||||
@ -284,7 +286,7 @@ abstract class MagiskInstaller {
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun flashBoot(): Boolean {
|
||||
private fun flashBoot(): Boolean {
|
||||
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
|
||||
return false
|
||||
arrayOf(
|
||||
@ -294,7 +296,7 @@ abstract class MagiskInstaller {
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun storeBoot(): Boolean {
|
||||
private fun storeBoot(): Boolean {
|
||||
val patched = SuFile.open(installDir, "new-boot.img")
|
||||
try {
|
||||
val os = tarOut?.let {
|
||||
@ -320,7 +322,7 @@ abstract class MagiskInstaller {
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun postOTA(): Boolean {
|
||||
private fun postOTA(): Boolean {
|
||||
val bootctl = SuFile("/data/adb/bootctl")
|
||||
try {
|
||||
withStreams(service.fetchBootctl().blockingGet().byteStream(), bootctl.suOutputStream()) {
|
||||
@ -345,6 +347,28 @@ abstract class MagiskInstaller {
|
||||
private fun String.fsh() = ShellUtils.fastCmd(this)
|
||||
private fun Array<String>.fsh() = ShellUtils.fastCmd(*this)
|
||||
|
||||
protected fun doPatchFile(patchFile: Uri) =
|
||||
extractZip() && handleFile(patchFile) && patchBoot() && storeBoot()
|
||||
|
||||
protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot()
|
||||
|
||||
protected fun secondSlot() =
|
||||
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
|
||||
|
||||
protected fun fixEnv(): Boolean {
|
||||
val context = get<Context>()
|
||||
val zip: File = context.cachedFile("magisk.zip")
|
||||
|
||||
installDir = SuFile("/data/adb/magisk")
|
||||
Shell.su("rm -rf /data/adb/magisk/*").exec()
|
||||
|
||||
if (!ShellUtils.checkSum("MD5", zip, Info.remote.magisk.md5))
|
||||
Networking.get(Info.remote.magisk.link).execForFile(zip)
|
||||
|
||||
zipUri = zip.toUri()
|
||||
return extractZip() && Shell.su("fix_env").exec().isSuccess
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
protected abstract fun operations(): Boolean
|
||||
|
101
app/src/main/java/com/topjohnwu/magisk/core/tasks/RepoUpdater.kt
Normal file
101
app/src/main/java/com/topjohnwu/magisk/core/tasks/RepoUpdater.kt
Normal file
@ -0,0 +1,101 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.data.database.RepoDao
|
||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.rxkotlin.toFlowable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
import timber.log.Timber
|
||||
import java.net.HttpURLConnection
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.collections.HashSet
|
||||
|
||||
class RepoUpdater(
|
||||
private val api: GithubApiServices,
|
||||
private val repoDB: RepoDao
|
||||
) {
|
||||
private fun loadRepos(repos: List<GithubRepoInfo>, cached: MutableSet<String>) =
|
||||
repos.toFlowable().parallel().runOn(Schedulers.io()).map {
|
||||
// Skip submission
|
||||
if (it.id == "submission")
|
||||
return@map
|
||||
val repo = repoDB.getRepo(it.id)?.apply { cached.remove(it.id) } ?: Repo(it.id)
|
||||
repo.runCatching {
|
||||
update(it.pushDate)
|
||||
repoDB.addRepo(this)
|
||||
}.getOrElse(Timber::e)
|
||||
}.sequential()
|
||||
|
||||
private fun loadPage(
|
||||
cached: MutableSet<String>,
|
||||
page: Int = 1,
|
||||
etag: String = ""
|
||||
): Flowable<Unit> = api.fetchRepos(page, etag).flatMap {
|
||||
it.error()?.also { throw it }
|
||||
it.response()?.run {
|
||||
if (code() == HttpURLConnection.HTTP_NOT_MODIFIED)
|
||||
return@run Flowable.error<Unit>(CachedException())
|
||||
|
||||
if (page == 1)
|
||||
repoDB.etagKey = headers()[Const.Key.ETAG_KEY].orEmpty().trimEtag()
|
||||
|
||||
val flow = loadRepos(body()!!, cached)
|
||||
if (headers()[Const.Key.LINK_KEY].orEmpty().contains("next")) {
|
||||
flow.mergeWith(loadPage(cached, page + 1))
|
||||
} else {
|
||||
flow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun forcedReload(cached: MutableSet<String>) =
|
||||
cached.toFlowable().parallel().runOn(Schedulers.io()).map {
|
||||
runCatching {
|
||||
Repo(it).update()
|
||||
}.getOrElse(Timber::e)
|
||||
}.sequential()
|
||||
|
||||
private fun String.trimEtag() = substring(indexOf('\"'), lastIndexOf('\"') + 1)
|
||||
|
||||
@Suppress("RedundantLambdaArrow")
|
||||
operator fun invoke(forced: Boolean) : Completable {
|
||||
return Flowable
|
||||
.fromCallable { Collections.synchronizedSet(HashSet(repoDB.repoIDList)) }
|
||||
.flatMap { cached ->
|
||||
loadPage(cached, etag = repoDB.etagKey).doOnComplete {
|
||||
repoDB.removeRepos(cached)
|
||||
}.onErrorResumeNext { it: Throwable ->
|
||||
if (it is CachedException) {
|
||||
if (forced)
|
||||
return@onErrorResumeNext forcedReload(cached)
|
||||
} else {
|
||||
Timber.e(it)
|
||||
}
|
||||
Flowable.empty()
|
||||
}
|
||||
}.ignoreElements()
|
||||
}
|
||||
|
||||
class CachedException : Exception()
|
||||
}
|
||||
|
||||
private val dateFormat: SimpleDateFormat =
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
data class GithubRepoInfo(
|
||||
val name: String,
|
||||
val pushed_at: String
|
||||
) {
|
||||
val id get() = name
|
||||
|
||||
@Transient
|
||||
val pushDate = dateFormat.parse(pushed_at)!!
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.get
|
||||
|
@ -1,11 +1,11 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Base64
|
||||
import android.util.Base64OutputStream
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.utils.PatchAPK.ALPHANUM
|
||||
import com.topjohnwu.magisk.di.koinModules
|
||||
import com.topjohnwu.magisk.utils.PatchAPK.ALPHANUM
|
||||
import com.topjohnwu.signing.CryptoUtils.readCertificate
|
||||
import com.topjohnwu.signing.CryptoUtils.readPrivateKey
|
||||
import com.topjohnwu.superuser.internal.InternalUtils
|
||||
@ -50,10 +50,14 @@ class Keygen: CertKeyProvider {
|
||||
|
||||
private val provider: CertKeyProvider
|
||||
|
||||
inner class KeyStoreProvider : CertKeyProvider {
|
||||
inner class KeyStoreProvider :
|
||||
CertKeyProvider {
|
||||
private val ks by lazy { init() }
|
||||
override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate }
|
||||
override val key by lazy { ks.getKey(ALIAS, PASSWORD) as PrivateKey }
|
||||
override val key by lazy { ks.getKey(
|
||||
ALIAS,
|
||||
PASSWORD
|
||||
) as PrivateKey }
|
||||
}
|
||||
|
||||
class TestProvider : CertKeyProvider {
|
||||
@ -113,8 +117,12 @@ class Keygen: CertKeyProvider {
|
||||
if (raw.isEmpty()) {
|
||||
ks.load(null)
|
||||
} else {
|
||||
GZIPInputStream(Base64.decode(raw, BASE64_FLAG).inputStream()).use {
|
||||
ks.load(it, PASSWORD)
|
||||
GZIPInputStream(Base64.decode(raw,
|
||||
BASE64_FLAG
|
||||
).inputStream()).use {
|
||||
ks.load(it,
|
||||
PASSWORD
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,10 +139,16 @@ class Keygen: CertKeyProvider {
|
||||
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||
|
||||
// Store them into keystore
|
||||
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert))
|
||||
ks.setKeyEntry(
|
||||
ALIAS, kp.private,
|
||||
PASSWORD, arrayOf(cert))
|
||||
val bytes = ByteArrayOutputStream()
|
||||
GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use {
|
||||
ks.store(it, PASSWORD)
|
||||
GZIPOutputStream(Base64OutputStream(bytes,
|
||||
BASE64_FLAG
|
||||
)).use {
|
||||
ks.store(it,
|
||||
PASSWORD
|
||||
)
|
||||
}
|
||||
Config.keyStoreRaw = bytes.toString("UTF-8")
|
||||
|
@ -1,13 +1,13 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.ResourceMgr
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.ResourceMgr
|
||||
import com.topjohnwu.magisk.extensions.langTagToLocale
|
||||
import com.topjohnwu.magisk.extensions.toLangTag
|
||||
import io.reactivex.Single
|
@ -1,15 +1,20 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.extensions.DynamicClassLoader
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.signing.JarMap
|
||||
import com.topjohnwu.signing.SignAPK
|
||||
import com.topjohnwu.superuser.Shell
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.FilterInputStream
|
||||
@ -41,4 +41,4 @@ class ProgressInputStream(
|
||||
}
|
||||
return sz
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.extensions.rawResource
|
||||
import com.topjohnwu.magisk.wrap
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
|
@ -0,0 +1,21 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
interface SafetyNetHelper {
|
||||
|
||||
val version: Int
|
||||
|
||||
fun attest()
|
||||
|
||||
interface Callback {
|
||||
fun onResponse(responseCode: Int)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val RESPONSE_ERR = 0x01
|
||||
const val CONNECTION_FAIL = 0x02
|
||||
|
||||
const val BASIC_PASS = 0x10
|
||||
const val CTS_PASS = 0x20
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@ -7,10 +7,10 @@ import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import androidx.work.*
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
@ -62,7 +62,10 @@ object Utils {
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
toast(R.string.open_link_failed_toast, Toast.LENGTH_SHORT)
|
||||
toast(
|
||||
R.string.open_link_failed_toast,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.view
|
||||
package com.topjohnwu.magisk.core.view
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
@ -9,13 +9,12 @@ import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toIcon
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
|
||||
import com.topjohnwu.magisk.Const.ID.UPDATE_NOTIFICATION_CHANNEL
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.getBitmap
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||
import com.topjohnwu.magisk.ui.SplashActivity
|
||||
|
||||
object Notifications {
|
||||
|
||||
@ -52,10 +51,13 @@ object Notifications {
|
||||
val stackBuilder = TaskStackBuilder.create(context)
|
||||
stackBuilder.addParentStack(SplashActivity::class.java.cmp(context.packageName))
|
||||
stackBuilder.addNextIntent(intent)
|
||||
val pendingIntent = stackBuilder.getPendingIntent(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID,
|
||||
val pendingIntent = stackBuilder.getPendingIntent(
|
||||
Const.ID.MAGISK_UPDATE_NOTIFICATION_ID,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val builder = updateBuilder(context)
|
||||
val builder = updateBuilder(
|
||||
context
|
||||
)
|
||||
.setContentTitle(context.getString(R.string.magisk_update_title))
|
||||
.setContentText(context.getString(R.string.manager_download_install))
|
||||
.setAutoCancel(true)
|
||||
@ -72,7 +74,9 @@ object Notifications {
|
||||
val pendingIntent = PendingIntent.getBroadcast(context,
|
||||
Const.ID.APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val builder = updateBuilder(context)
|
||||
val builder = updateBuilder(
|
||||
context
|
||||
)
|
||||
.setContentTitle(context.getString(R.string.manager_update_title))
|
||||
.setContentText(context.getString(R.string.manager_download_install))
|
||||
.setAutoCancel(true)
|
||||
@ -87,7 +91,9 @@ object Notifications {
|
||||
val pendingIntent = PendingIntent.getBroadcast(context,
|
||||
Const.ID.DTBO_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val builder = updateBuilder(context)
|
||||
val builder = updateBuilder(
|
||||
context
|
||||
)
|
||||
.setContentTitle(context.getString(R.string.dtbo_patched_title))
|
||||
.setContentText(context.getString(R.string.dtbo_patched_reboot))
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.view
|
||||
package com.topjohnwu.magisk.core.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@ -10,17 +10,18 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toAdaptiveIcon
|
||||
import androidx.core.graphics.drawable.toIcon
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.magisk.extensions.getBitmap
|
||||
import com.topjohnwu.magisk.ui.SplashActivity
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
|
||||
object Shortcuts {
|
||||
|
||||
fun setup(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= 25) {
|
||||
val manager = context.getSystemService<ShortcutManager>()
|
||||
manager?.dynamicShortcuts = getShortCuts(context)
|
||||
manager?.dynamicShortcuts =
|
||||
getShortCuts(context)
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,19 +78,6 @@ object Shortcuts {
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_extension))
|
||||
.setRank(3)
|
||||
.build()
|
||||
)
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "downloads")
|
||||
.setShortLabel(context.getString(R.string.downloads))
|
||||
.setIntent(
|
||||
Intent(intent)
|
||||
.putExtra(Const.Key.OPEN_SECTION, "downloads")
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_cloud_download))
|
||||
.setRank(2)
|
||||
.build()
|
||||
)
|
67
app/src/main/java/com/topjohnwu/magisk/data/database/Repo.kt
Normal file
67
app/src/main/java/com/topjohnwu/magisk/data/database/Repo.kt
Normal file
@ -0,0 +1,67 @@
|
||||
@file:JvmMultifileClass
|
||||
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
|
||||
interface RepoBase {
|
||||
|
||||
fun getRepos(offset: Int, limit: Int = LIMIT): List<Repo>
|
||||
fun searchRepos(query: String, offset: Int, limit: Int = LIMIT): List<Repo>
|
||||
|
||||
@Query("SELECT * FROM repos WHERE id = :id AND versionCode > :versionCode LIMIT 1")
|
||||
fun getUpdatableRepoById(id: String, versionCode: Int): Repo?
|
||||
|
||||
@Query("SELECT * FROM repos WHERE id = :id LIMIT 1")
|
||||
fun getRepoById(id: String): Repo?
|
||||
|
||||
companion object {
|
||||
const val LIMIT = 10
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface RepoByUpdatedDao : RepoBase {
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY last_update DESC LIMIT :limit OFFSET :offset")
|
||||
override fun getRepos(offset: Int, limit: Int): List<Repo>
|
||||
|
||||
@Query(
|
||||
"""SELECT *
|
||||
FROM repos
|
||||
WHERE
|
||||
(author LIKE '%' || :query || '%') ||
|
||||
(name LIKE '%' || :query || '%') ||
|
||||
(description LIKE '%' || :query || '%')
|
||||
ORDER BY last_update DESC
|
||||
LIMIT :limit
|
||||
OFFSET :offset"""
|
||||
)
|
||||
override fun searchRepos(query: String, offset: Int, limit: Int): List<Repo>
|
||||
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface RepoByNameDao : RepoBase {
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY name COLLATE NOCASE LIMIT :limit OFFSET :offset")
|
||||
override fun getRepos(offset: Int, limit: Int): List<Repo>
|
||||
|
||||
@Query(
|
||||
"""SELECT *
|
||||
FROM repos
|
||||
WHERE
|
||||
(author LIKE '%' || :query || '%') ||
|
||||
(name LIKE '%' || :query || '%') ||
|
||||
(description LIKE '%' || :query || '%')
|
||||
ORDER BY name COLLATE NOCASE
|
||||
LIMIT :limit
|
||||
OFFSET :offset"""
|
||||
)
|
||||
override fun searchRepos(query: String, offset: Int, limit: Int): List<Repo>
|
||||
|
||||
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.*
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
|
||||
@Database(version = 6, entities = [Repo::class, RepoEtag::class])
|
||||
abstract class RepoDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun repoDao() : RepoDao
|
||||
abstract fun repoByUpdatedDao(): RepoByUpdatedDao
|
||||
abstract fun repoByNameDao(): RepoByNameDao
|
||||
}
|
||||
|
||||
@Dao
|
||||
|
@ -1,8 +1,8 @@
|
||||
package com.topjohnwu.magisk.data.network
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
||||
import com.topjohnwu.magisk.tasks.GithubRepoInfo
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.tasks.GithubRepoInfo
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Single
|
||||
import okhttp3.ResponseBody
|
||||
@ -78,4 +78,4 @@ interface GithubApiServices {
|
||||
@Query("sort") sort: String = "pushed",
|
||||
@Query("per_page") count: Int = 100): Flowable<Result<List<GithubRepoInfo>>>
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
@ -1,20 +1,18 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.data.database.SuLogDao
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class LogRepository(
|
||||
private val logDao: SuLogDao
|
||||
) {
|
||||
|
||||
fun fetchLogs() = logDao.fetchAll().map { it.wrap() }
|
||||
fun fetchLogs() = logDao.fetchAll()
|
||||
|
||||
fun fetchMagiskLogs() = Single.fromCallable {
|
||||
Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").exec().out
|
||||
@ -28,11 +26,4 @@ class LogRepository(
|
||||
|
||||
fun insert(log: MagiskLog) = logDao.insert(log)
|
||||
|
||||
private fun List<MagiskLog>.wrap(): List<WrappedMagiskLog> {
|
||||
val day = TimeUnit.DAYS.toMillis(1)
|
||||
return groupBy { it.time / day }
|
||||
.map { WrappedMagiskLog(it.key * day, it.value) }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.extensions.getLabel
|
||||
import com.topjohnwu.magisk.extensions.packageName
|
||||
@ -24,7 +24,8 @@ class MagiskRepository(
|
||||
Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
|
||||
Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate()
|
||||
Config.Value.CANARY_DEBUG_CHANNEL -> apiRaw.fetchCanaryDebugUpdate()
|
||||
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(Config.customChannelUrl)
|
||||
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(
|
||||
Config.customChannelUrl)
|
||||
else -> throw IllegalArgumentException()
|
||||
}.flatMap {
|
||||
// If remote version is lower than current installed, try switching to beta
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
class StringRepository(
|
||||
private val api: GithubRawServices
|
||||
@ -12,4 +12,4 @@ class StringRepository(
|
||||
fun getMetadata(repo: Repo) = api.fetchModuleInfo(repo.id, "module.prop")
|
||||
|
||||
fun getReadme(repo: Repo) = api.fetchModuleInfo(repo.id, "README.md")
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.topjohnwu.magisk.utils.RxBus
|
||||
import org.koin.core.qualifier.named
|
||||
@ -23,6 +24,7 @@ val applicationModule = module {
|
||||
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
|
||||
single { ActivityTracker() }
|
||||
factory { get<ActivityTracker>().foreground ?: NullActivity }
|
||||
single { LocalBroadcastManager.getInstance(get()) }
|
||||
}
|
||||
|
||||
private fun createDEContext(context: Context): Context {
|
||||
|
@ -2,8 +2,12 @@ package com.topjohnwu.magisk.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.topjohnwu.magisk.data.database.*
|
||||
import com.topjohnwu.magisk.tasks.RepoUpdater
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import com.topjohnwu.magisk.core.tasks.RepoUpdater
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase
|
||||
import com.topjohnwu.magisk.data.database.SuLogDatabase
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
@ -11,7 +15,10 @@ val databaseModule = module {
|
||||
single { PolicyDao(get()) }
|
||||
single { SettingsDao() }
|
||||
single { StringDao() }
|
||||
single { createRepoDatabase(get()).repoDao() }
|
||||
single { createRepoDatabase(get()) }
|
||||
single { get<RepoDatabase>().repoDao() }
|
||||
single { get<RepoDatabase>().repoByNameDao() }
|
||||
single { get<RepoDatabase>().repoByUpdatedDao() }
|
||||
single { createSuLogDatabase(get(Protected)).suLogDao() }
|
||||
single { RepoUpdater(get(), get()) }
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import android.content.Context
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.net.Networking
|
||||
|
@ -1,25 +1,36 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.legacy.flash.FlashViewModel
|
||||
import com.topjohnwu.magisk.legacy.surequest.SuRequestViewModel
|
||||
import com.topjohnwu.magisk.ui.MainViewModel
|
||||
import com.topjohnwu.magisk.ui.flash.FlashViewModel
|
||||
import com.topjohnwu.magisk.ui.hide.HideViewModel
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||
import com.topjohnwu.magisk.ui.request.RequestViewModel
|
||||
import com.topjohnwu.magisk.ui.safetynet.SafetynetViewModel
|
||||
import com.topjohnwu.magisk.ui.settings.SettingsViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
||||
import com.topjohnwu.magisk.ui.theme.ThemeViewModel
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val viewModelModules = module {
|
||||
viewModel { MainViewModel() }
|
||||
viewModel { HideViewModel(get()) }
|
||||
viewModel { HomeViewModel(get()) }
|
||||
viewModel { SuperuserViewModel(get(), get(), get(), get()) }
|
||||
viewModel { HideViewModel(get(), get()) }
|
||||
viewModel { LogViewModel(get()) }
|
||||
viewModel { ModuleViewModel(get(), get(), get()) }
|
||||
viewModel { LogViewModel(get(), get()) }
|
||||
viewModel { RequestViewModel() }
|
||||
viewModel { SafetynetViewModel(get()) }
|
||||
viewModel { SettingsViewModel(get()) }
|
||||
viewModel { SuperuserViewModel(get(), get(), get()) }
|
||||
viewModel { ThemeViewModel() }
|
||||
viewModel { InstallViewModel() }
|
||||
viewModel { MainViewModel() }
|
||||
|
||||
// Legacy
|
||||
viewModel { (action: String, file: Uri, additional: Uri) ->
|
||||
FlashViewModel(action, file, additional, get())
|
||||
}
|
||||
|
@ -52,9 +52,9 @@ fun <T> Observable<T>.subscribeK(
|
||||
|
||||
fun <T> Single<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onSuccess: OnSuccessListener<T> = {}
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onSuccess, onError)
|
||||
.subscribe(onNext, onError)
|
||||
|
||||
fun <T> Maybe<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
@ -198,5 +198,8 @@ fun <T> ObservableField<T>.toObservable(): Observable<T> {
|
||||
|
||||
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) })
|
||||
inline fun <T1, T2, R> zip(
|
||||
t1: Single<T1>,
|
||||
t2: Single<T2>,
|
||||
crossinline zipper: (T1, T2) -> R
|
||||
) = Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })
|
@ -28,14 +28,17 @@ import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.FileProvider
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.utils.DynamicClassLoader
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.lang.reflect.Array as JArray
|
||||
|
||||
val packageName: String get() = get<Context>().packageName
|
||||
@ -283,7 +286,7 @@ fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.getDrawable(thi
|
||||
* with respect to RTL layout direction
|
||||
*/
|
||||
fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
|
||||
if (SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
|
||||
resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
) {
|
||||
return end to start
|
||||
@ -294,10 +297,10 @@ fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
|
||||
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)
|
||||
inline fun <reified T> T.DynamicClassLoader(apk: File) =
|
||||
DynamicClassLoader(apk, T::class.java.classLoader)
|
||||
|
||||
fun Context.unwrap() : Context {
|
||||
fun Context.unwrap(): Context {
|
||||
var context = this
|
||||
while (true) {
|
||||
if (context is ContextWrapper)
|
||||
@ -309,3 +312,38 @@ fun Context.unwrap() : Context {
|
||||
}
|
||||
|
||||
fun Uri.writeTo(file: File) = toFile().copyTo(file)
|
||||
|
||||
fun Context.hasPermissions(vararg permissions: String) = permissions.all {
|
||||
ContextCompat.checkSelfPermission(this, it) == PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
private val securityLevelFormatter get() = SimpleDateFormat("yyyy-MM-dd",
|
||||
currentLocale
|
||||
)
|
||||
|
||||
/** Friendly reminder to seek newer roms or install oem updates. */
|
||||
val isDeviceSecure: Boolean
|
||||
get() {
|
||||
val latestPermittedTime = Calendar.getInstance().apply {
|
||||
time = securityLevelDate
|
||||
add(Calendar.MONTH, 2)
|
||||
}.time.time
|
||||
return now in 0..latestPermittedTime
|
||||
}
|
||||
val securityLevelDate get() = securityLevelFormatter.parseOrNull(securityLevel) ?: Date(0)
|
||||
val securityLevel
|
||||
get() = if (SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Build.VERSION.SECURITY_PATCH
|
||||
} else {
|
||||
null
|
||||
} ?: "1970-01-01" //never
|
||||
|
||||
val isSAR
|
||||
get() = ShellUtils
|
||||
.fastCmd("grep_prop ro.build.system_root_image")
|
||||
.let { it.isNotEmpty() && it.toBoolean() }
|
||||
|
||||
val isAB
|
||||
get() = ShellUtils
|
||||
.fastCmd("grep_prop ro.build.ab_update")
|
||||
.let { it.isNotEmpty() && it.toBoolean() }
|
||||
|
@ -1,11 +1,13 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.os.Build
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
@ -100,6 +102,9 @@ fun Locale.toLangTag(): String {
|
||||
}
|
||||
}
|
||||
|
||||
fun SimpleDateFormat.parseOrNull(date: String) =
|
||||
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
|
||||
|
||||
// Reflection hacks
|
||||
|
||||
private val loadClass = ClassLoader::class.java.getMethod("loadClass", String::class.java)
|
||||
|
@ -1,6 +1,6 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
@ -11,4 +11,6 @@ fun reboot(reason: String = if (Info.recovery) "recovery" else "") {
|
||||
}
|
||||
|
||||
fun File.suOutputStream() = SuFileOutputStream(this)
|
||||
fun File.suInputStream() = SuFileInputStream(this)
|
||||
fun File.suInputStream() = SuFileInputStream(this)
|
||||
|
||||
val hasRoot get() = Shell.rootAccess()
|
||||
|
@ -2,7 +2,15 @@ package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.res.Resources
|
||||
|
||||
val specialChars = arrayOf('!', '@', '#', '$', '%', '&', '?')
|
||||
val specialChars = arrayOf('!', '@', '#', '$', '%', '&', '?')
|
||||
|
||||
fun String.replaceRandomWithSpecial(passes: Int): String {
|
||||
var string = this
|
||||
repeat(passes) {
|
||||
string = string.replaceRandomWithSpecial()
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
fun String.replaceRandomWithSpecial(): String {
|
||||
var random: Char
|
||||
|
@ -1,6 +1,6 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
@ -14,7 +14,22 @@ fun String.toTime(format: DateFormat) = try {
|
||||
-1L
|
||||
}
|
||||
|
||||
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", currentLocale) }
|
||||
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", currentLocale) }
|
||||
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, currentLocale) }
|
||||
val timeFormatTime by lazy { SimpleDateFormat("h:mm a", currentLocale) }
|
||||
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss",
|
||||
currentLocale
|
||||
) }
|
||||
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'",
|
||||
currentLocale
|
||||
) }
|
||||
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM,
|
||||
currentLocale
|
||||
) }
|
||||
val timeFormatTime by lazy { SimpleDateFormat("h:mm a",
|
||||
currentLocale
|
||||
) }
|
||||
val timeDateFormat by lazy {
|
||||
DateFormat.getDateTimeInstance(
|
||||
DateFormat.DEFAULT,
|
||||
DateFormat.DEFAULT,
|
||||
currentLocale
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.TransitionManager
|
||||
|
||||
fun View.setOnViewReadyListener(callback: () -> Unit) = addOnGlobalLayoutListener(true, callback)
|
||||
|
||||
@ -11,4 +15,9 @@ fun View.addOnGlobalLayoutListener(oneShot: Boolean = false, callback: () -> Uni
|
||||
if (oneShot) viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
callback()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
fun ViewGroup.startAnimations() {
|
||||
val transition = AutoTransition().setInterpolator(FastOutSlowInInterpolator()).setDuration(400)
|
||||
TransitionManager.beginDelayedTransition(this, transition)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.ui.flash
|
||||
package com.topjohnwu.magisk.legacy.flash
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@ -6,22 +6,22 @@ import android.content.pm.ActivityInfo
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.base.BaseActivity
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.databinding.ActivityFlashBinding
|
||||
import com.topjohnwu.magisk.extensions.snackbar
|
||||
import com.topjohnwu.magisk.intent
|
||||
import com.topjohnwu.magisk.model.events.BackPressEvent
|
||||
import com.topjohnwu.magisk.model.events.PermissionEvent
|
||||
import com.topjohnwu.magisk.model.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.ui.base.BaseUIActivity
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import java.io.File
|
||||
|
||||
open class FlashActivity : BaseActivity<FlashViewModel, ActivityFlashBinding>() {
|
||||
open class FlashActivity : BaseUIActivity<FlashViewModel, ActivityFlashBinding>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.activity_flash
|
||||
override val themeRes: Int = R.style.MagiskTheme_Flashing
|
@ -0,0 +1,110 @@
|
||||
package com.topjohnwu.magisk.legacy.flash
|
||||
|
||||
import android.Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import androidx.core.os.postDelayed
|
||||
import androidx.databinding.ObservableArrayList
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.extensions.*
|
||||
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
|
||||
import com.topjohnwu.magisk.model.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.model.flash.FlashResultListener
|
||||
import com.topjohnwu.magisk.model.flash.Flashing
|
||||
import com.topjohnwu.magisk.model.flash.Patching
|
||||
import com.topjohnwu.magisk.ui.base.BaseViewModel
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class FlashViewModel(
|
||||
action: String,
|
||||
installer: Uri,
|
||||
uri: Uri,
|
||||
private val resources: Resources
|
||||
) : BaseViewModel(), FlashResultListener {
|
||||
|
||||
val canShowReboot = Shell.rootAccess()
|
||||
val showRestartTitle = KObservableField(false)
|
||||
|
||||
val behaviorText = KObservableField(resources.getString(R.string.flashing))
|
||||
|
||||
val items = DiffObservableList(ComparableRvItem.callback)
|
||||
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item ->
|
||||
item.bind(itemBinding)
|
||||
itemBinding.bindExtra(BR.viewModel, this@FlashViewModel)
|
||||
}
|
||||
|
||||
private val outItems = ObservableArrayList<String>()
|
||||
private val logItems = Collections.synchronizedList(mutableListOf<String>())
|
||||
|
||||
init {
|
||||
outItems.sendUpdatesTo(items) { it.map { ConsoleRvItem(it) } }
|
||||
outItems.copyNewInputInto(logItems)
|
||||
|
||||
state = State.LOADING
|
||||
|
||||
when (action) {
|
||||
Const.Value.FLASH_ZIP -> Flashing
|
||||
.Install(installer, outItems, logItems, this)
|
||||
.exec()
|
||||
Const.Value.UNINSTALL -> Flashing
|
||||
.Uninstall(installer, outItems, logItems, this)
|
||||
.exec()
|
||||
Const.Value.FLASH_MAGISK -> Patching
|
||||
.Direct(installer, outItems, logItems, this)
|
||||
.exec()
|
||||
Const.Value.FLASH_INACTIVE_SLOT -> Patching
|
||||
.SecondSlot(installer, outItems, logItems, this)
|
||||
.exec()
|
||||
Const.Value.PATCH_FILE -> Patching
|
||||
.File(installer, uri, outItems, logItems, this)
|
||||
.exec()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResult(isSuccess: Boolean) {
|
||||
state = if (isSuccess) State.LOADED else State.LOADING_FAILED
|
||||
behaviorText.value = when {
|
||||
isSuccess -> resources.getString(R.string.done)
|
||||
else -> resources.getString(R.string.failure)
|
||||
}
|
||||
|
||||
if (isSuccess) {
|
||||
Handler().postDelayed(500) {
|
||||
showRestartTitle.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun savePressed() = withPermissions(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE)
|
||||
.map { now }
|
||||
.map { it.toTime(timeFormatStandard) }
|
||||
.map { Const.MAGISK_INSTALL_LOG_FILENAME.format(it) }
|
||||
.map { File(Config.downloadDirectory, it) }
|
||||
.map { file ->
|
||||
file.bufferedWriter().use { writer ->
|
||||
logItems.forEach {
|
||||
writer.write(it)
|
||||
writer.newLine()
|
||||
}
|
||||
}
|
||||
file.path
|
||||
}
|
||||
.subscribeK { SnackbarEvent(it).publish() }
|
||||
.add()
|
||||
|
||||
fun restartPressed() = reboot()
|
||||
|
||||
fun backPressed() = back()
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.ui.surequest
|
||||
package com.topjohnwu.magisk.legacy.surequest
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
@ -6,16 +6,16 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Window
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.base.BaseActivity
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler.REQUEST
|
||||
import com.topjohnwu.magisk.databinding.ActivityRequestBinding
|
||||
import com.topjohnwu.magisk.model.events.DieEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewActionEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||
import com.topjohnwu.magisk.utils.SuHandler
|
||||
import com.topjohnwu.magisk.utils.SuHandler.REQUEST
|
||||
import com.topjohnwu.magisk.ui.base.BaseUIActivity
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestBinding>() {
|
||||
open class SuRequestActivity : BaseUIActivity<SuRequestViewModel, ActivityRequestBinding>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.activity_request
|
||||
override val themeRes: Int = R.style.MagiskTheme_SU
|
||||
@ -36,7 +36,11 @@ open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestB
|
||||
}
|
||||
|
||||
fun runHandler(action: String?) {
|
||||
SuHandler(this, action, intent.extras)
|
||||
SuCallbackHandler(
|
||||
this,
|
||||
action,
|
||||
intent.extras
|
||||
)
|
||||
finish()
|
||||
}
|
||||
|
@ -0,0 +1,123 @@
|
||||
package com.topjohnwu.magisk.legacy.surequest
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.CountDownTimer
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.ALLOW
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.DENY
|
||||
import com.topjohnwu.magisk.core.su.SuRequestHandler
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem
|
||||
import com.topjohnwu.magisk.model.events.DieEvent
|
||||
import com.topjohnwu.magisk.ui.base.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
|
||||
class SuRequestViewModel(
|
||||
private val pm: PackageManager,
|
||||
private val policyDB: PolicyDao,
|
||||
private val timeoutPrefs: SharedPreferences,
|
||||
private val res: Resources
|
||||
) : BaseViewModel() {
|
||||
|
||||
val icon = KObservableField<Drawable?>(null)
|
||||
val title = KObservableField("")
|
||||
val packageName = KObservableField("")
|
||||
|
||||
val denyText = KObservableField(res.getString(R.string.deny))
|
||||
val warningText = KObservableField<CharSequence>(res.getString(R.string.su_warning))
|
||||
|
||||
val selectedItemPosition = KObservableField(0)
|
||||
|
||||
private val items = DiffObservableList(ComparableRvItem.callback)
|
||||
private val itemBinding = ItemBinding.of<ComparableRvItem<*>> { binding, _, item ->
|
||||
item.bind(binding)
|
||||
}
|
||||
|
||||
val adapter = BindingListViewAdapter<ComparableRvItem<*>>(1).apply {
|
||||
itemBinding = this@SuRequestViewModel.itemBinding
|
||||
setItems(items)
|
||||
}
|
||||
|
||||
private val handler = Handler()
|
||||
|
||||
fun grantPressed() {
|
||||
handler.cancelTimer()
|
||||
if (BiometricHelper.isEnabled) {
|
||||
withView {
|
||||
BiometricHelper.authenticate(this) {
|
||||
handler.respond(ALLOW)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handler.respond(ALLOW)
|
||||
}
|
||||
}
|
||||
|
||||
fun denyPressed() {
|
||||
handler.respond(DENY)
|
||||
}
|
||||
|
||||
fun spinnerTouched(): Boolean {
|
||||
handler.cancelTimer()
|
||||
return false
|
||||
}
|
||||
|
||||
fun handleRequest(intent: Intent): Boolean {
|
||||
return handler.start(intent)
|
||||
}
|
||||
|
||||
private inner class Handler : SuRequestHandler(pm, policyDB) {
|
||||
|
||||
fun respond(action: Int) {
|
||||
val pos = selectedItemPosition.value
|
||||
timeoutPrefs.edit().putInt(policy.packageName, pos).apply()
|
||||
respond(action, Config.Value.TIMEOUT_LIST[pos])
|
||||
}
|
||||
|
||||
fun cancelTimer() {
|
||||
timer.cancel()
|
||||
denyText.value = res.getString(R.string.deny)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
res.getStringArray(R.array.allow_timeout)
|
||||
.map { SpinnerRvItem(it) }
|
||||
.let { items.update(it) }
|
||||
|
||||
icon.value = policy.applicationInfo.loadIcon(pm)
|
||||
title.value = policy.appName
|
||||
packageName.value = policy.packageName
|
||||
selectedItemPosition.value = timeoutPrefs.getInt(policy.packageName, 0)
|
||||
|
||||
// Override timer
|
||||
val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
|
||||
timer = object : CountDownTimer(millis, 1000) {
|
||||
override fun onTick(remains: Long) {
|
||||
denyText.value = "${res.getString(R.string.deny)} (${remains / 1000})"
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
denyText.value = res.getString(R.string.deny)
|
||||
respond(DENY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRespond() {
|
||||
// Kill activity after response
|
||||
DieEvent().publish()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -14,3 +14,14 @@ class HideAppInfo(
|
||||
val processes = info.packageInfo?.processes?.distinct() ?: listOf(info.packageName)
|
||||
|
||||
}
|
||||
|
||||
data class StatefulProcess(
|
||||
val name: String,
|
||||
val packageName: String,
|
||||
val isHidden: Boolean
|
||||
)
|
||||
|
||||
class ProcessHideApp(
|
||||
val info: HideAppInfo,
|
||||
val processes: List<StatefulProcess>
|
||||
)
|
@ -3,10 +3,11 @@ package com.topjohnwu.magisk.model.entity
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.ALLOW
|
||||
import com.topjohnwu.magisk.extensions.now
|
||||
import com.topjohnwu.magisk.extensions.timeFormatTime
|
||||
import com.topjohnwu.magisk.extensions.toTime
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.ALLOW
|
||||
|
||||
@Entity(tableName = "logs")
|
||||
data class MagiskLog(
|
||||
@ -23,11 +24,6 @@ data class MagiskLog(
|
||||
@Ignore val timeString = time.toTime(timeFormatTime)
|
||||
}
|
||||
|
||||
data class WrappedMagiskLog(
|
||||
val time: Long,
|
||||
val items: List<MagiskLog>
|
||||
)
|
||||
|
||||
fun MagiskPolicy.toLog(
|
||||
toUid: Int,
|
||||
fromPid: Int,
|
||||
|
@ -2,13 +2,13 @@ package com.topjohnwu.magisk.model.entity.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.extensions.cachedFile
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.model.entity.MagiskJson
|
||||
import com.topjohnwu.magisk.model.entity.ManagerJson
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||
import com.topjohnwu.magisk.core.model.ManagerJson
|
||||
import kotlinx.android.parcel.IgnoredOnParcel
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.io.File
|
||||
@ -59,7 +59,7 @@ sealed class DownloadSubject : Parcelable {
|
||||
val magisk: MagiskJson = Info.remote.magisk
|
||||
|
||||
@Parcelize
|
||||
protected data class Flash(
|
||||
data class Flash(
|
||||
override val configuration: Configuration
|
||||
) : Magisk() {
|
||||
override val url: String get() = magisk.link
|
||||
@ -72,7 +72,7 @@ sealed class DownloadSubject : Parcelable {
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
protected class Uninstall : Magisk() {
|
||||
class Uninstall : Magisk() {
|
||||
override val configuration: Configuration get() = Configuration.Uninstall
|
||||
override val url: String get() = Info.remote.uninstaller.link
|
||||
|
||||
@ -83,7 +83,7 @@ sealed class DownloadSubject : Parcelable {
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
protected class Download : Magisk() {
|
||||
class Download : Magisk() {
|
||||
override val configuration: Configuration get() = Configuration.Download
|
||||
override val url: String get() = magisk.link
|
||||
|
||||
@ -103,4 +103,4 @@ sealed class DownloadSubject : Parcelable {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.R
|
||||
|
||||
class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() {
|
||||
open class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_console
|
||||
|
||||
override fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {
|
||||
@ -23,4 +23,8 @@ class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() {
|
||||
|
||||
override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other)
|
||||
override fun itemSameAs(other: ConsoleRvItem) = item == other.item
|
||||
}
|
||||
|
||||
class ConsoleItem(item: String) : ConsoleRvItem(item) {
|
||||
override val layoutRes = R.layout.item_console_md2
|
||||
}
|
@ -1,92 +1,81 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.startAnimations
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
||||
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
|
||||
import com.topjohnwu.magisk.model.events.HideProcessEvent
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import com.topjohnwu.magisk.model.entity.ProcessHideApp
|
||||
import com.topjohnwu.magisk.model.entity.StatefulProcess
|
||||
import com.topjohnwu.magisk.model.observer.Observer
|
||||
import com.topjohnwu.magisk.ui.hide.HideViewModel
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import com.topjohnwu.magisk.utils.RxBus
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
|
||||
ComparableRvItem<HideRvItem>() {
|
||||
class HideItem(val item: ProcessHideApp) : ComparableRvItem<HideItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_hide_app
|
||||
override val layoutRes = R.layout.item_hide_md2
|
||||
|
||||
val packageName = item.info.info.packageName.orEmpty()
|
||||
val items = item.processes.map { HideProcessItem(it) }
|
||||
|
||||
val packageName = item.info.packageName.orEmpty()
|
||||
val items = DiffObservableList(callback).also {
|
||||
val items = item.processes.map {
|
||||
val isHidden = targets.any { target ->
|
||||
packageName == target.packageName && it == target.process
|
||||
}
|
||||
HideProcessRvItem(packageName, it, isHidden)
|
||||
}
|
||||
it.update(items)
|
||||
}
|
||||
val isHiddenState = KObservableField(currentState)
|
||||
val isExpanded = KObservableField(false)
|
||||
val itemsChecked = KObservableField(0)
|
||||
val itemsCheckedPercent = Observer(itemsChecked) {
|
||||
(itemsChecked.value.toFloat() / items.size * 100).roundToInt()
|
||||
}
|
||||
|
||||
private val itemsProcess get() = items.filterIsInstance<HideProcessRvItem>()
|
||||
|
||||
private val currentState
|
||||
get() = when (itemsProcess.count { it.isHidden.value }) {
|
||||
items.size -> IndeterminateState.CHECKED
|
||||
in 1 until items.size -> IndeterminateState.INDETERMINATE
|
||||
else -> IndeterminateState.UNCHECKED
|
||||
}
|
||||
/** [toggle] depends on this functionality */
|
||||
private val isHidden get() = itemsChecked.value == items.size
|
||||
|
||||
init {
|
||||
itemsProcess.forEach {
|
||||
it.isHidden.addOnPropertyChangedCallback { isHiddenState.value = currentState }
|
||||
}
|
||||
items.forEach { it.isHidden.addOnPropertyChangedCallback { recalculateChecked() } }
|
||||
recalculateChecked()
|
||||
}
|
||||
|
||||
fun toggle() {
|
||||
val desiredState = when (isHiddenState.value) {
|
||||
IndeterminateState.INDETERMINATE,
|
||||
IndeterminateState.UNCHECKED -> true
|
||||
IndeterminateState.CHECKED -> false
|
||||
}
|
||||
itemsProcess.forEach { it.isHidden.value = desiredState }
|
||||
isHiddenState.value = currentState
|
||||
fun collapse(v: View) {
|
||||
(v.parent.parent as? ViewGroup)?.startAnimations()
|
||||
isExpanded.value = false
|
||||
}
|
||||
|
||||
fun toggleExpansion() {
|
||||
if (items.size <= 1) return
|
||||
fun toggle(v: View) {
|
||||
(v.parent as? ViewGroup)?.startAnimations()
|
||||
isExpanded.toggle()
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: HideRvItem): Boolean = items.all { other.items.contains(it) }
|
||||
override fun itemSameAs(other: HideRvItem): Boolean = item.info == other.item.info
|
||||
fun toggle(viewModel: HideViewModel): Boolean {
|
||||
// contract implies that isHidden == all checked
|
||||
if (!isHidden) {
|
||||
items.filterNot { it.isHidden.value }
|
||||
} else {
|
||||
items
|
||||
}.forEach { it.toggle(viewModel) }
|
||||
return true
|
||||
}
|
||||
|
||||
private fun recalculateChecked() {
|
||||
itemsChecked.value = items.count { it.isHidden.value }
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: HideItem): Boolean = item == other.item
|
||||
override fun itemSameAs(other: HideItem): Boolean = item.info == other.item.info
|
||||
|
||||
}
|
||||
|
||||
class HideProcessRvItem(
|
||||
val packageName: String,
|
||||
val process: String,
|
||||
isHidden: Boolean
|
||||
) : ComparableRvItem<HideProcessRvItem>() {
|
||||
class HideProcessItem(val item: StatefulProcess) : ComparableRvItem<HideProcessItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_hide_process
|
||||
override val layoutRes = R.layout.item_hide_process_md2
|
||||
|
||||
val isHidden = KObservableField(isHidden)
|
||||
val isHidden = KObservableField(item.isHidden)
|
||||
|
||||
private val rxBus: RxBus by inject()
|
||||
|
||||
init {
|
||||
this.isHidden.addOnPropertyChangedCallback {
|
||||
rxBus.post(HideProcessEvent(this@HideProcessRvItem))
|
||||
}
|
||||
fun toggle(viewModel: HideViewModel) {
|
||||
isHidden.toggle()
|
||||
viewModel.toggleItem(this)
|
||||
}
|
||||
|
||||
fun toggle() = isHidden.toggle()
|
||||
override fun contentSameAs(other: HideProcessItem) = item == other.item
|
||||
override fun itemSameAs(other: HideProcessItem) = item.name == other.item.name
|
||||
|
||||
override fun contentSameAs(other: HideProcessRvItem): Boolean = itemSameAs(other)
|
||||
override fun itemSameAs(other: HideProcessRvItem): Boolean =
|
||||
packageName == other.packageName && process == other.process
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,116 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
|
||||
sealed class HomeItem : ComparableRvItem<HomeItem>() {
|
||||
|
||||
abstract val icon: Int
|
||||
abstract val title: Int
|
||||
abstract val link: String
|
||||
|
||||
override val layoutRes = R.layout.item_developer_link
|
||||
|
||||
override fun contentSameAs(other: HomeItem) = itemSameAs(other)
|
||||
override fun itemSameAs(other: HomeItem) = this == other
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is HomeItem) return false
|
||||
return icon == other.icon && title == other.title && link == other.link
|
||||
}
|
||||
|
||||
override fun hashCode() =
|
||||
icon.hashCode() + title.hashCode() + link.hashCode() + layoutRes.hashCode()
|
||||
|
||||
// region Children
|
||||
sealed class PayPal : HomeItem() {
|
||||
override val icon = R.drawable.ic_paypal
|
||||
override val title = R.string.home_item_paypal
|
||||
override val link = "https://paypal.me/%s"
|
||||
|
||||
// region Children
|
||||
object App : PayPal() {
|
||||
override val link = super.link.format("diareuse")
|
||||
}
|
||||
|
||||
object Mainline : PayPal() {
|
||||
override val link = super.link.format("topjohnwu")
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
object Patreon : HomeItem() {
|
||||
override val icon = R.drawable.ic_patreon
|
||||
override val title = R.string.home_item_patreon
|
||||
override val link = Const.Url.PATREON_URL
|
||||
}
|
||||
|
||||
sealed class Twitter : HomeItem() {
|
||||
override val icon = R.drawable.ic_twitter
|
||||
override val title = R.string.home_item_twitter
|
||||
override val link = "https://twitter.com/%s"
|
||||
|
||||
// region Children
|
||||
object App : Twitter() {
|
||||
override val link = super.link.format("diareuse")
|
||||
}
|
||||
|
||||
object Mainline : Twitter() {
|
||||
override val link = super.link.format("topjohnwu")
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
object Github : HomeItem() {
|
||||
override val icon = R.drawable.ic_github
|
||||
override val title = R.string.home_item_source
|
||||
override val link = Const.Url.SOURCE_CODE_URL
|
||||
}
|
||||
|
||||
object Xda : HomeItem() {
|
||||
override val icon = R.drawable.ic_xda
|
||||
override val title = R.string.home_item_xda
|
||||
override val link = Const.Url.XDA_THREAD
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
sealed class DeveloperItem : ComparableRvItem<DeveloperItem>() {
|
||||
|
||||
abstract val items: List<HomeItem>
|
||||
abstract val name: Int
|
||||
|
||||
override val layoutRes = R.layout.item_developer
|
||||
|
||||
override fun contentSameAs(other: DeveloperItem) = itemSameAs(other)
|
||||
override fun itemSameAs(other: DeveloperItem) = this == other
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is DeveloperItem) return false
|
||||
return name == other.name && items == other.items
|
||||
}
|
||||
|
||||
override fun hashCode() = name.hashCode() + items.hashCode() + layoutRes.hashCode()
|
||||
|
||||
//region Children
|
||||
object Mainline : DeveloperItem() {
|
||||
override val items =
|
||||
listOf(HomeItem.PayPal.Mainline, HomeItem.Patreon, HomeItem.Twitter.Mainline)
|
||||
override val name = R.string.home_links_mainline
|
||||
}
|
||||
|
||||
object App : DeveloperItem() {
|
||||
override val items =
|
||||
listOf(HomeItem.PayPal.App, HomeItem.Twitter.App)
|
||||
override val name = R.string.home_links_app
|
||||
}
|
||||
|
||||
object Project : DeveloperItem() {
|
||||
override val items =
|
||||
listOf(HomeItem.Github, HomeItem.Xda)
|
||||
override val name = R.string.home_links_project
|
||||
}
|
||||
//endregion
|
||||
|
||||
}
|
@ -1,73 +1,39 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.extensions.timeFormatMedium
|
||||
import com.topjohnwu.magisk.extensions.timeDateFormat
|
||||
import com.topjohnwu.magisk.extensions.toTime
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
|
||||
class LogRvItem : ComparableRvItem<LogRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_page_log
|
||||
class LogItem(val item: MagiskLog) : ObservableItem<LogItem>() {
|
||||
|
||||
val items = DiffObservableList(callback)
|
||||
override val layoutRes = R.layout.item_log_access_md2
|
||||
|
||||
fun update(list: List<LogItemRvItem>) {
|
||||
list.firstOrNull()?.isExpanded?.value = true
|
||||
items.update(list)
|
||||
}
|
||||
val date = item.time.toTime(timeDateFormat)
|
||||
var isTop = false
|
||||
@Bindable get
|
||||
set(value) {
|
||||
field = value
|
||||
notifyChange(BR.top)
|
||||
}
|
||||
var isBottom = false
|
||||
@Bindable get
|
||||
set(value) {
|
||||
field = value
|
||||
notifyChange(BR.bottom)
|
||||
}
|
||||
|
||||
//two of these will never be present, safe to assume it's unique
|
||||
override fun contentSameAs(other: LogRvItem): Boolean = false
|
||||
override fun itemSameAs(other: LogItem) = item.appName == other.item.appName
|
||||
|
||||
override fun itemSameAs(other: LogRvItem): Boolean = false
|
||||
}
|
||||
|
||||
class LogItemRvItem(
|
||||
item: WrappedMagiskLog
|
||||
) : ComparableRvItem<LogItemRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_superuser_log
|
||||
|
||||
val date = item.time.toTime(timeFormatMedium)
|
||||
val items: List<ComparableRvItem<*>> = item.items.map { LogItemEntryRvItem(it) }
|
||||
val isExpanded = KObservableField(false)
|
||||
|
||||
fun toggle() = isExpanded.toggle()
|
||||
|
||||
override fun contentSameAs(other: LogItemRvItem): Boolean {
|
||||
if (items.size != other.items.size) return false
|
||||
return items.all { it in other.items }
|
||||
}
|
||||
|
||||
override fun itemSameAs(other: LogItemRvItem): Boolean = date == other.date
|
||||
}
|
||||
|
||||
class LogItemEntryRvItem(val item: MagiskLog) : ComparableRvItem<LogItemEntryRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_superuser_log_entry
|
||||
|
||||
val isExpanded = KObservableField(false)
|
||||
|
||||
fun toggle() = isExpanded.toggle()
|
||||
|
||||
override fun contentSameAs(other: LogItemEntryRvItem) = item == other.item
|
||||
|
||||
override fun itemSameAs(other: LogItemEntryRvItem) = item.appName == other.item.appName
|
||||
}
|
||||
|
||||
class MagiskLogRvItem : ComparableRvItem<MagiskLogRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_page_magisk_log
|
||||
|
||||
val items = DiffObservableList(callback)
|
||||
|
||||
fun update(list: List<ConsoleRvItem>) {
|
||||
items.update(list)
|
||||
}
|
||||
|
||||
//two of these will never be present, safe to assume it's unique
|
||||
override fun contentSameAs(other: MagiskLogRvItem): Boolean = false
|
||||
|
||||
override fun itemSameAs(other: MagiskLogRvItem): Boolean = false
|
||||
override fun contentSameAs(other: LogItem) = item.fromUid == other.item.fromUid &&
|
||||
item.toUid == other.item.toUid &&
|
||||
item.fromPid == other.item.fromPid &&
|
||||
item.packageName == other.item.packageName &&
|
||||
item.command == other.item.command &&
|
||||
item.action == other.item.action &&
|
||||
item.time == other.item.time &&
|
||||
isTop == other.isTop &&
|
||||
isBottom == other.isBottom
|
||||
}
|
||||
|
@ -1,75 +1,162 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.model.module.Module
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.module.Module
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
|
||||
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
|
||||
object SafeModeNotice : ComparableRvItem<SafeModeNotice>() {
|
||||
override val layoutRes = R.layout.item_safe_mode_notice
|
||||
|
||||
override val layoutRes: Int = R.layout.item_module
|
||||
override fun onBindingBound(binding: ViewDataBinding) {
|
||||
super.onBindingBound(binding)
|
||||
val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams
|
||||
params?.isFullSpan = true
|
||||
}
|
||||
|
||||
val lastActionNotice = KObservableField("")
|
||||
val isChecked = KObservableField(item.enable)
|
||||
val isDeletable = KObservableField(item.remove)
|
||||
override fun contentSameAs(other: SafeModeNotice) = this == other
|
||||
override fun itemSameAs(other: SafeModeNotice) = this === other
|
||||
}
|
||||
|
||||
init {
|
||||
isChecked.addOnPropertyChangedCallback {
|
||||
when (it) {
|
||||
true -> {
|
||||
item.enable = true
|
||||
notice(R.string.disable_file_removed)
|
||||
}
|
||||
false -> {
|
||||
item.enable = false
|
||||
notice(R.string.disable_file_created)
|
||||
}
|
||||
}
|
||||
object InstallModule : ComparableRvItem<InstallModule>() {
|
||||
override val layoutRes = R.layout.item_module_download
|
||||
|
||||
override fun onBindingBound(binding: ViewDataBinding) {
|
||||
super.onBindingBound(binding)
|
||||
val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams
|
||||
params?.isFullSpan = true
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: InstallModule) = this == other
|
||||
override fun itemSameAs(other: InstallModule) = this === other
|
||||
}
|
||||
|
||||
class SectionTitle(
|
||||
val title: Int,
|
||||
_button: Int = 0,
|
||||
_icon: Int = 0
|
||||
) : ObservableItem<SectionTitle>() {
|
||||
override val layoutRes = R.layout.item_section_md2
|
||||
|
||||
var button = _button
|
||||
@Bindable get
|
||||
set(value) {
|
||||
field = value
|
||||
notifyChange(BR.button)
|
||||
}
|
||||
isDeletable.addOnPropertyChangedCallback {
|
||||
when (it) {
|
||||
true -> {
|
||||
item.remove = true
|
||||
notice(R.string.remove_file_created)
|
||||
}
|
||||
false -> {
|
||||
item.remove = false
|
||||
notice(R.string.remove_file_deleted)
|
||||
}
|
||||
}
|
||||
var icon = _icon
|
||||
@Bindable get
|
||||
set(value) {
|
||||
field = value
|
||||
notifyChange(BR.icon)
|
||||
}
|
||||
when {
|
||||
item.updated -> notice(R.string.update_file_created)
|
||||
item.remove -> notice(R.string.remove_file_created)
|
||||
var hasButton = button != 0 || icon != 0
|
||||
@Bindable get
|
||||
set(value) {
|
||||
field = value
|
||||
notifyChange(BR.hasButton)
|
||||
}
|
||||
|
||||
override fun onBindingBound(binding: ViewDataBinding) {
|
||||
super.onBindingBound(binding)
|
||||
val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams
|
||||
params?.isFullSpan = true
|
||||
}
|
||||
|
||||
override fun itemSameAs(other: SectionTitle): Boolean = this === other
|
||||
override fun contentSameAs(other: SectionTitle): Boolean = this === other
|
||||
}
|
||||
|
||||
sealed class RepoItem(val item: Repo) : ObservableItem<RepoItem>() {
|
||||
override val layoutRes: Int = R.layout.item_repo_md2
|
||||
|
||||
val progress = KObservableField(0)
|
||||
var isUpdate = false
|
||||
@Bindable get
|
||||
protected set(value) {
|
||||
field = value
|
||||
notifyChange(BR.update)
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: RepoItem): Boolean = item == other.item
|
||||
override fun itemSameAs(other: RepoItem): Boolean = item.id == other.item.id
|
||||
|
||||
class Update(item: Repo) : RepoItem(item) {
|
||||
init {
|
||||
isUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle() = isChecked.toggle()
|
||||
fun toggleDelete() = isDeletable.toggle()
|
||||
class Remote(item: Repo) : RepoItem(item)
|
||||
}
|
||||
|
||||
private fun notice(@StringRes info: Int) {
|
||||
lastActionNotice.value = get<Resources>().getString(info)
|
||||
class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
|
||||
|
||||
override val layoutRes = R.layout.item_module_md2
|
||||
|
||||
@get:Bindable
|
||||
var repo: Repo? = null
|
||||
set(value) {
|
||||
field = value
|
||||
notifyChange(BR.repo)
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var isEnabled = item.enable
|
||||
set(value) {
|
||||
field = value
|
||||
item.enable = value
|
||||
notifyChange(BR.enabled)
|
||||
}
|
||||
@get:Bindable
|
||||
var isRemoved = item.remove
|
||||
set(value) {
|
||||
field = value
|
||||
item.remove = value
|
||||
notifyChange(BR.removed)
|
||||
}
|
||||
|
||||
val isUpdated get() = item.updated
|
||||
val isModified get() = isRemoved || item.updated
|
||||
|
||||
fun toggle() {
|
||||
isEnabled = !isEnabled
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: ModuleRvItem): Boolean = item.version == other.item.version
|
||||
fun delete(viewModel: ModuleViewModel) {
|
||||
isRemoved = !isRemoved
|
||||
viewModel.updateActiveState()
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: ModuleItem): Boolean = item.version == other.item.version
|
||||
&& item.versionCode == other.item.versionCode
|
||||
&& item.description == other.item.description
|
||||
&& item.name == other.item.name
|
||||
|
||||
override fun itemSameAs(other: ModuleRvItem): Boolean = item.id == other.item.id
|
||||
override fun itemSameAs(other: ModuleItem): Boolean = item.id == other.item.id
|
||||
|
||||
}
|
||||
|
||||
class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
|
||||
abstract class ObservableItem<T> : ComparableRvItem<T>(), Observable {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_repo
|
||||
private val list = PropertyChangeRegistry()
|
||||
|
||||
override fun contentSameAs(other: RepoRvItem): Boolean = item == other.item
|
||||
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
|
||||
list.remove(callback ?: return)
|
||||
}
|
||||
|
||||
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
|
||||
}
|
||||
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
|
||||
list.add(callback ?: return)
|
||||
}
|
||||
|
||||
fun notifyChange(id: Int) = list.notifyChange(this, id)
|
||||
|
||||
}
|
||||
|
@ -2,51 +2,52 @@ package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.model.MagiskPolicy
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
|
||||
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import com.topjohnwu.magisk.utils.RxBus
|
||||
|
||||
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_policy
|
||||
class PolicyItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyItem>() {
|
||||
override val layoutRes = R.layout.item_policy_md2
|
||||
|
||||
val isExpanded = KObservableField(false)
|
||||
val isEnabled = KObservableField(item.policy == MagiskPolicy.ALLOW)
|
||||
val shouldNotify = KObservableField(item.notification)
|
||||
val shouldLog = KObservableField(item.logging)
|
||||
|
||||
fun toggle() = isExpanded.toggle()
|
||||
|
||||
private val rxBus: RxBus by inject()
|
||||
|
||||
private val currentStateItem
|
||||
private val updatedPolicy
|
||||
get() = item.copy(
|
||||
policy = if (isEnabled.value) MagiskPolicy.ALLOW else MagiskPolicy.DENY,
|
||||
notification = shouldNotify.value,
|
||||
logging = shouldLog.value
|
||||
)
|
||||
|
||||
init {
|
||||
isEnabled.addOnPropertyChangedCallback {
|
||||
it ?: return@addOnPropertyChangedCallback
|
||||
rxBus.post(PolicyEnableEvent(this@PolicyRvItem, it))
|
||||
}
|
||||
shouldNotify.addOnPropertyChangedCallback {
|
||||
it ?: return@addOnPropertyChangedCallback
|
||||
rxBus.post(PolicyUpdateEvent.Notification(currentStateItem))
|
||||
}
|
||||
shouldLog.addOnPropertyChangedCallback {
|
||||
it ?: return@addOnPropertyChangedCallback
|
||||
rxBus.post(PolicyUpdateEvent.Log(currentStateItem))
|
||||
fun toggle(viewModel: SuperuserViewModel) {
|
||||
if (isExpanded.value) {
|
||||
toggle()
|
||||
return
|
||||
}
|
||||
isEnabled.toggle()
|
||||
viewModel.togglePolicy(this, isEnabled.value)
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: PolicyRvItem): Boolean = itemSameAs(other)
|
||||
override fun itemSameAs(other: PolicyRvItem): Boolean = item.uid == other.item.uid
|
||||
}
|
||||
fun toggle() {
|
||||
isExpanded.toggle()
|
||||
}
|
||||
|
||||
fun toggleNotify(viewModel: SuperuserViewModel) {
|
||||
shouldNotify.toggle()
|
||||
viewModel.updatePolicy(PolicyUpdateEvent.Notification(updatedPolicy))
|
||||
}
|
||||
|
||||
fun toggleLog(viewModel: SuperuserViewModel) {
|
||||
shouldLog.toggle()
|
||||
viewModel.updatePolicy(PolicyUpdateEvent.Log(updatedPolicy))
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: PolicyItem) = itemSameAs(other)
|
||||
override fun itemSameAs(other: PolicyItem) = item.uid == other.item.uid
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,191 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.utils.TransitiveText
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.get
|
||||
import kotlin.properties.ObservableProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
sealed class SettingsItem : ObservableItem<SettingsItem>() {
|
||||
|
||||
open val icon: Int get() = 0
|
||||
open val title: TransitiveText get() = TransitiveText.EMPTY
|
||||
|
||||
@get:Bindable
|
||||
open val description: TransitiveText get() = TransitiveText.EMPTY
|
||||
|
||||
@get:Bindable
|
||||
var isEnabled by bindable(true, BR.enabled)
|
||||
|
||||
protected open val isFullSpan get() = false
|
||||
|
||||
@CallSuper
|
||||
open fun onPressed(view: View, callback: Callback) {
|
||||
callback.onItemChanged(view, this)
|
||||
|
||||
// notify only after the callback invocation; callback can invalidate the backing data,
|
||||
// which wouldn't be recognized with reverse approach
|
||||
notifyChange(BR.description)
|
||||
}
|
||||
|
||||
open fun refresh() {}
|
||||
|
||||
override fun onBindingBound(binding: ViewDataBinding) {
|
||||
super.onBindingBound(binding)
|
||||
if (isFullSpan) {
|
||||
val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams
|
||||
params?.isFullSpan = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun itemSameAs(other: SettingsItem) = this === other
|
||||
override fun contentSameAs(other: SettingsItem) = itemSameAs(other)
|
||||
|
||||
protected inline fun <T> bindable(
|
||||
initialValue: T,
|
||||
fieldId: Int,
|
||||
crossinline setter: (T) -> Unit = {}
|
||||
) = object : ObservableProperty<T>(initialValue) {
|
||||
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) {
|
||||
setter(newValue)
|
||||
notifyChange(fieldId)
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
interface Callback {
|
||||
fun onItemPressed(view: View, item: SettingsItem)
|
||||
fun onItemChanged(view: View, item: SettingsItem)
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
abstract class Value<T> : SettingsItem() {
|
||||
|
||||
@get:Bindable
|
||||
abstract var value: T
|
||||
|
||||
protected inline fun bindableValue(
|
||||
initialValue: T,
|
||||
crossinline setter: (T) -> Unit
|
||||
) = bindable(initialValue, BR.value, setter)
|
||||
|
||||
}
|
||||
|
||||
abstract class Toggle : Value<Boolean>() {
|
||||
|
||||
override val layoutRes = R.layout.item_settings_toggle
|
||||
|
||||
override fun onPressed(view: View, callback: Callback) {
|
||||
callback.onItemPressed(view, this)
|
||||
value = !value
|
||||
super.onPressed(view, callback)
|
||||
}
|
||||
|
||||
fun onTouched(view: View, callback: Callback, event: MotionEvent): Boolean {
|
||||
if (event.action == MotionEvent.ACTION_UP) {
|
||||
onPressed(view, callback)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class Input : Value<String>(), KoinComponent {
|
||||
|
||||
override val layoutRes = R.layout.item_settings_input
|
||||
open val showStrip = true
|
||||
|
||||
protected val resources get() = get<Resources>()
|
||||
protected abstract val intermediate: String?
|
||||
|
||||
override fun onPressed(view: View, callback: Callback) {
|
||||
callback.onItemPressed(view, this)
|
||||
MagiskDialog(view.context)
|
||||
.applyTitle(title.getText(resources))
|
||||
.applyView(getView(view.context))
|
||||
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
titleRes = android.R.string.ok
|
||||
onClick {
|
||||
intermediate?.let { result ->
|
||||
preventDismiss = false
|
||||
value = result
|
||||
it.dismiss()
|
||||
super.onPressed(view, callback)
|
||||
return@onClick
|
||||
}
|
||||
preventDismiss = true
|
||||
}
|
||||
}
|
||||
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
titleRes = android.R.string.cancel
|
||||
}
|
||||
.reveal()
|
||||
}
|
||||
|
||||
abstract fun getView(context: Context): View
|
||||
|
||||
}
|
||||
|
||||
abstract class Selector : Value<Int>(), KoinComponent {
|
||||
|
||||
override val layoutRes = R.layout.item_settings_selector
|
||||
|
||||
protected val resources get() = get<Resources>()
|
||||
|
||||
abstract val entries: Array<out CharSequence>
|
||||
abstract val entryValues: Array<out CharSequence>
|
||||
|
||||
@get:Bindable
|
||||
val selectedEntry
|
||||
get() = entries.getOrNull(value)
|
||||
|
||||
override fun onPressed(view: View, callback: Callback) {
|
||||
if (entries.isEmpty() || entryValues.isEmpty()) return
|
||||
callback.onItemPressed(view, this)
|
||||
MagiskDialog(view.context)
|
||||
.applyTitle(title.getText(resources))
|
||||
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
titleRes = android.R.string.cancel
|
||||
}
|
||||
.applyAdapter(entries) {
|
||||
value = it
|
||||
notifyChange(BR.selectedEntry)
|
||||
super.onPressed(view, callback)
|
||||
}
|
||||
.reveal()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class Blank : SettingsItem() {
|
||||
|
||||
override val layoutRes = R.layout.item_settings_blank
|
||||
|
||||
override fun onPressed(view: View, callback: Callback) {
|
||||
callback.onItemPressed(view, this)
|
||||
super.onPressed(view, callback)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class Section : SettingsItem() {
|
||||
|
||||
override val layoutRes = R.layout.item_settings_section
|
||||
override val isFullSpan get() = true
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
|
||||
sealed class TappableHeadlineItem : ComparableRvItem<TappableHeadlineItem>() {
|
||||
|
||||
abstract val title: Int
|
||||
abstract val icon: Int
|
||||
|
||||
override val layoutRes = R.layout.item_tappable_headline
|
||||
|
||||
override fun itemSameAs(other: TappableHeadlineItem) =
|
||||
this === other
|
||||
|
||||
override fun contentSameAs(other: TappableHeadlineItem) =
|
||||
title == other.title && icon == other.icon
|
||||
|
||||
// --- listener
|
||||
|
||||
interface Listener {
|
||||
|
||||
fun onItemPressed(item: TappableHeadlineItem)
|
||||
|
||||
}
|
||||
|
||||
// --- objects
|
||||
|
||||
object Hide : TappableHeadlineItem() {
|
||||
override val title = R.string.magisk_hide_md2
|
||||
override val icon = R.drawable.ic_hide_md2
|
||||
}
|
||||
|
||||
object Safetynet : TappableHeadlineItem() {
|
||||
override val title = R.string.safetyNet
|
||||
override val icon = R.drawable.ic_safetynet_md2
|
||||
}
|
||||
|
||||
object ThemeMode : TappableHeadlineItem() {
|
||||
override val title = R.string.settings_dark_mode_title
|
||||
override val icon = R.drawable.ic_day_night
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
|
||||
class TextItem(val text: Int) : ComparableRvItem<TextItem>() {
|
||||
override val layoutRes = R.layout.item_text
|
||||
|
||||
override fun onBindingBound(binding: ViewDataBinding) {
|
||||
super.onBindingBound(binding)
|
||||
val params = binding.root.layoutParams as? StaggeredGridLayoutManager.LayoutParams
|
||||
params?.isFullSpan = true
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: TextItem) = text == other.text
|
||||
override fun itemSameAs(other: TextItem) = contentSameAs(other)
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
|
||||
class ThemeItem(val theme: Theme) : ComparableRvItem<ThemeItem>() {
|
||||
|
||||
override val layoutRes = R.layout.item_theme
|
||||
|
||||
override fun contentSameAs(other: ThemeItem) = itemSameAs(other)
|
||||
override fun itemSameAs(other: ThemeItem) = theme == other.theme
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user