Merge branch 'WIP'
This commit is contained in:
commit
2813d2031a
@ -1,6 +1,6 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
kapt {
|
kapt {
|
||||||
@ -16,6 +16,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'com.topjohnwu.magisk'
|
applicationId 'com.topjohnwu.magisk'
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
multiDexEnabled true
|
||||||
versionName configProps['appVersion']
|
versionName configProps['appVersion']
|
||||||
versionCode configProps['appVersionCode'] as Integer
|
versionCode configProps['appVersionCode'] as Integer
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
@ -30,6 +31,10 @@ android {
|
|||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataBinding {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@ -37,25 +42,19 @@ dependencies {
|
|||||||
implementation project(':net')
|
implementation project(':net')
|
||||||
implementation project(':shared')
|
implementation project(':shared')
|
||||||
implementation project(':signing')
|
implementation project(':signing')
|
||||||
|
|
||||||
implementation 'com.github.topjohnwu:jtar:1.0.0'
|
implementation 'com.github.topjohnwu:jtar:1.0.0'
|
||||||
implementation 'net.sourceforge.streamsupport:android-retrostreams:1.7.0'
|
implementation 'net.sourceforge.streamsupport:android-retrostreams:1.7.0'
|
||||||
implementation 'com.github.sevar83:indeterminate-checkbox:1.0.5'
|
implementation 'com.github.sevar83:indeterminate-checkbox:1.0.5'
|
||||||
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
implementation 'com.github.skoumalcz:teanity:0.3.3'
|
||||||
|
implementation 'com.ncapdevi:frag-nav:3.2.0'
|
||||||
|
|
||||||
def markwonVersion = '3.0.0'
|
def markwonVersion = '3.0.0'
|
||||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||||
implementation "ru.noties.markwon:html:${markwonVersion}"
|
implementation "ru.noties.markwon:html:${markwonVersion}"
|
||||||
implementation "ru.noties.markwon:image-svg:${markwonVersion}"
|
implementation "ru.noties.markwon:image-svg:${markwonVersion}"
|
||||||
|
|
||||||
def androidXVersion = "1.0.0"
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
|
||||||
implementation "androidx.preference:preference:${androidXVersion}"
|
|
||||||
implementation "androidx.recyclerview:recyclerview:${androidXVersion}"
|
|
||||||
implementation "androidx.cardview:cardview:${androidXVersion}"
|
|
||||||
implementation "com.google.android.material:material:${androidXVersion}"
|
|
||||||
implementation 'androidx.work:work-runtime:2.0.1'
|
|
||||||
implementation 'androidx.transition:transition:1.1.0-beta01'
|
|
||||||
|
|
||||||
def libsuVersion = '2.5.0'
|
def libsuVersion = '2.5.0'
|
||||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||||
@ -63,4 +62,19 @@ dependencies {
|
|||||||
def butterKnifeVersion = '10.1.0'
|
def butterKnifeVersion = '10.1.0'
|
||||||
implementation "com.jakewharton:butterknife-runtime:${butterKnifeVersion}"
|
implementation "com.jakewharton:butterknife-runtime:${butterKnifeVersion}"
|
||||||
kapt "com.jakewharton:butterknife-compiler:${butterKnifeVersion}"
|
kapt "com.jakewharton:butterknife-compiler:${butterKnifeVersion}"
|
||||||
|
|
||||||
|
def koin = "2.0.0-rc-2"
|
||||||
|
implementation "org.koin:koin-core:${koin}"
|
||||||
|
implementation "org.koin:koin-android:${koin}"
|
||||||
|
implementation "org.koin:koin-androidx-viewmodel:${koin}"
|
||||||
|
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||||
|
implementation 'androidx.preference:preference:1.0.0'
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha04'
|
||||||
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
|
implementation 'com.google.android.material:material:1.1.0-alpha05'
|
||||||
|
implementation 'androidx.work:work-runtime:2.0.1'
|
||||||
|
implementation 'androidx.transition:transition:1.1.0-beta01'
|
||||||
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
}
|
}
|
||||||
|
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
@ -29,6 +29,11 @@
|
|||||||
void onResponse(int);
|
void onResponse(int);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Keep all fragment constructors
|
||||||
|
-keepclassmembers class * extends androidx.fragment.app.Fragment {
|
||||||
|
public <init>(...);
|
||||||
|
}
|
||||||
|
|
||||||
# DelegateWorker
|
# DelegateWorker
|
||||||
-keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker
|
-keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:name="a.e"
|
android:name="a.e"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/MagiskTheme"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||||
|
|
||||||
@ -35,7 +35,7 @@
|
|||||||
android:name="a.f"
|
android:name="a.f"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
android:screenOrientation="nosensor"
|
android:screenOrientation="nosensor"
|
||||||
android:theme="@style/AppTheme.NoDrawer" />
|
android:theme="@style/MagiskTheme.Flashing" />
|
||||||
|
|
||||||
<!-- Superuser -->
|
<!-- Superuser -->
|
||||||
|
|
||||||
@ -44,7 +44,7 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:theme="@style/SuRequest" />
|
android:theme="@style/MagiskTheme.SU" />
|
||||||
|
|
||||||
<!-- Receiver -->
|
<!-- Receiver -->
|
||||||
|
|
||||||
@ -74,4 +74,4 @@
|
|||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -1,101 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.data.database.MagiskDB;
|
|
||||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
|
||||||
import com.topjohnwu.magisk.utils.RootUtils;
|
|
||||||
import com.topjohnwu.net.Networking;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
|
||||||
|
|
||||||
public class App extends Application implements Application.ActivityLifecycleCallbacks {
|
|
||||||
|
|
||||||
public static App self;
|
|
||||||
public static Context deContext;
|
|
||||||
public static ThreadPoolExecutor THREAD_POOL;
|
|
||||||
|
|
||||||
// Global resources
|
|
||||||
public SharedPreferences prefs;
|
|
||||||
public MagiskDB mDB;
|
|
||||||
public RepoDatabaseHelper repoDB;
|
|
||||||
private volatile BaseActivity foreground;
|
|
||||||
|
|
||||||
static {
|
|
||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
|
||||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER | Shell.FLAG_USE_MAGISK_BUSYBOX);
|
|
||||||
Shell.Config.verboseLogging(BuildConfig.DEBUG);
|
|
||||||
Shell.Config.addInitializers(RootUtils.class);
|
|
||||||
Shell.Config.setTimeout(2);
|
|
||||||
THREAD_POOL = (ThreadPoolExecutor) AsyncTask.THREAD_POOL_EXECUTOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(Context base) {
|
|
||||||
super.attachBaseContext(base);
|
|
||||||
self = this;
|
|
||||||
deContext = base;
|
|
||||||
registerActivityLifecycleCallbacks(this);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 24) {
|
|
||||||
deContext = base.createDeviceProtectedStorageContext();
|
|
||||||
deContext.moveSharedPreferencesFrom(base,
|
|
||||||
PreferenceManager.getDefaultSharedPreferencesName(base));
|
|
||||||
}
|
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(deContext);
|
|
||||||
mDB = new MagiskDB(base);
|
|
||||||
|
|
||||||
Networking.init(base);
|
|
||||||
LocaleManager.setLocale(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
|
||||||
super.onConfigurationChanged(newConfig);
|
|
||||||
LocaleManager.setLocale(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static BaseActivity foreground() {
|
|
||||||
return self.foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityStarted(@NonNull Activity activity) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void onActivityResumed(@NonNull Activity activity) {
|
|
||||||
foreground = (BaseActivity) activity;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void onActivityPaused(@NonNull Activity activity) {
|
|
||||||
foreground = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityStopped(@NonNull Activity activity) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle bundle) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityDestroyed(@NonNull Activity activity) {}
|
|
||||||
}
|
|
129
app/src/main/java/com/topjohnwu/magisk/App.kt
Normal file
129
app/src/main/java/com/topjohnwu/magisk/App.kt
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package com.topjohnwu.magisk
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.AsyncTask
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.multidex.MultiDex
|
||||||
|
import com.topjohnwu.magisk.data.database.MagiskDB
|
||||||
|
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
|
||||||
|
import com.topjohnwu.magisk.di.koinModules
|
||||||
|
import com.topjohnwu.magisk.utils.LocaleManager
|
||||||
|
import com.topjohnwu.magisk.utils.RootUtils
|
||||||
|
import com.topjohnwu.magisk.utils.inject
|
||||||
|
import com.topjohnwu.net.Networking
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor
|
||||||
|
|
||||||
|
open class App : Application(), Application.ActivityLifecycleCallbacks {
|
||||||
|
|
||||||
|
lateinit var protectedContext: Context
|
||||||
|
|
||||||
|
@Deprecated("Use dependency injection")
|
||||||
|
val prefs: SharedPreferences by inject()
|
||||||
|
@Deprecated("Use dependency injection")
|
||||||
|
val DB: MagiskDB by inject()
|
||||||
|
@Deprecated("Use dependency injection")
|
||||||
|
val repoDB: RepoDatabaseHelper by inject()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var foreground: Activity? = null
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
MultiDex.install(base)
|
||||||
|
Timber.plant(Timber.DebugTree())
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
androidContext(this@App)
|
||||||
|
modules(koinModules)
|
||||||
|
}
|
||||||
|
|
||||||
|
protectedContext = baseContext
|
||||||
|
self = this
|
||||||
|
deContext = base
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 24) {
|
||||||
|
protectedContext = base.createDeviceProtectedStorageContext()
|
||||||
|
deContext = protectedContext
|
||||||
|
deContext.moveSharedPreferencesFrom(base, base.defaultPrefsName)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerActivityLifecycleCallbacks(this)
|
||||||
|
|
||||||
|
Networking.init(base)
|
||||||
|
LocaleManager.setLocale(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
LocaleManager.setLocale(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
//region ActivityLifecycleCallbacks
|
||||||
|
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||||
|
|
||||||
|
override fun onActivityStarted(activity: Activity) {}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun onActivityResumed(activity: Activity) {
|
||||||
|
foreground = activity
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun onActivityPaused(activity: Activity) {
|
||||||
|
foreground = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityStopped(activity: Activity) {}
|
||||||
|
|
||||||
|
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||||
|
|
||||||
|
override fun onActivityDestroyed(activity: Activity) {}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
private val Context.defaultPrefsName get() = "${packageName}_preferences"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
@Deprecated("Use dependency injection")
|
||||||
|
@JvmStatic
|
||||||
|
lateinit var self: App
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
@Deprecated("Use dependency injection; replace with protectedContext")
|
||||||
|
@JvmStatic
|
||||||
|
lateinit var deContext: Context
|
||||||
|
|
||||||
|
@Deprecated("Use Rx or similar")
|
||||||
|
@JvmField
|
||||||
|
var THREAD_POOL: ThreadPoolExecutor
|
||||||
|
|
||||||
|
init {
|
||||||
|
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||||
|
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
|
||||||
|
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||||
|
Shell.Config.addInitializers(RootUtils::class.java)
|
||||||
|
Shell.Config.setTimeout(2)
|
||||||
|
THREAD_POOL = AsyncTask.THREAD_POOL_EXECUTOR as ThreadPoolExecutor
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("")
|
||||||
|
@JvmStatic
|
||||||
|
fun foreground(): Activity? {
|
||||||
|
val app: App by inject()
|
||||||
|
return app.foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,6 @@ package com.topjohnwu.magisk;
|
|||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.util.Xml;
|
import android.util.Xml;
|
||||||
|
|
||||||
import androidx.collection.ArrayMap;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
import com.topjohnwu.superuser.Shell;
|
import com.topjohnwu.superuser.Shell;
|
||||||
import com.topjohnwu.superuser.ShellUtils;
|
import com.topjohnwu.superuser.ShellUtils;
|
||||||
@ -17,6 +15,8 @@ import org.xmlpull.v1.XmlPullParserException;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import androidx.collection.ArrayMap;
|
||||||
|
|
||||||
public class Config {
|
public class Config {
|
||||||
|
|
||||||
// Current status
|
// Current status
|
||||||
@ -109,14 +109,14 @@ public class Config {
|
|||||||
public static void export() {
|
public static void export() {
|
||||||
// Flush prefs to disk
|
// Flush prefs to disk
|
||||||
App app = App.self;
|
App app = App.self;
|
||||||
app.prefs.edit().commit();
|
app.getPrefs().edit().commit();
|
||||||
File xml = new File(App.deContext.getFilesDir().getParent() + "/shared_prefs",
|
File xml = new File(App.deContext.getFilesDir().getParent() + "/shared_prefs",
|
||||||
app.getPackageName() + "_preferences.xml");
|
app.getPackageName() + "_preferences.xml");
|
||||||
Shell.su(Utils.fmt("cat %s > /data/adb/%s", xml, Const.MANAGER_CONFIGS)).exec();
|
Shell.su(Utils.fmt("cat %s > /data/adb/%s", xml, Const.MANAGER_CONFIGS)).exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void initialize() {
|
public static void initialize() {
|
||||||
SharedPreferences pref = App.self.prefs;
|
SharedPreferences pref = App.self.getPrefs();
|
||||||
SharedPreferences.Editor editor = pref.edit();
|
SharedPreferences.Editor editor = pref.edit();
|
||||||
File config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS);
|
File config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS);
|
||||||
if (config.exists()) {
|
if (config.exists()) {
|
||||||
@ -238,19 +238,19 @@ public class Config {
|
|||||||
App app = App.self;
|
App app = App.self;
|
||||||
switch (getConfigType(key)) {
|
switch (getConfigType(key)) {
|
||||||
case PREF_INT:
|
case PREF_INT:
|
||||||
return (T) (Integer) app.prefs.getInt(key, getDef(key));
|
return (T) (Integer) app.getPrefs().getInt(key, getDef(key));
|
||||||
case PREF_STR_INT:
|
case PREF_STR_INT:
|
||||||
return (T) (Integer) Utils.getPrefsInt(app.prefs, key, getDef(key));
|
return (T) (Integer) Utils.getPrefsInt(app.getPrefs(), key, getDef(key));
|
||||||
case PREF_BOOL:
|
case PREF_BOOL:
|
||||||
return (T) (Boolean) app.prefs.getBoolean(key, getDef(key));
|
return (T) (Boolean) app.getPrefs().getBoolean(key, getDef(key));
|
||||||
case PREF_STR:
|
case PREF_STR:
|
||||||
return (T) app.prefs.getString(key, getDef(key));
|
return (T) app.getPrefs().getString(key, getDef(key));
|
||||||
case DB_INT:
|
case DB_INT:
|
||||||
return (T) (Integer) app.mDB.getSettings(key, getDef(key));
|
return (T) (Integer) app.getDB().getSettings(key, getDef(key));
|
||||||
case DB_BOOL:
|
case DB_BOOL:
|
||||||
return (T) (Boolean) (app.mDB.getSettings(key, getDef(key) ? 1 : 0) != 0);
|
return (T) (Boolean) (app.getDB().getSettings(key, getDef(key) ? 1 : 0) != 0);
|
||||||
case DB_STR:
|
case DB_STR:
|
||||||
return (T) app.mDB.getStrings(key, getDef(key));
|
return (T) app.getDB().getStrings(key, getDef(key));
|
||||||
}
|
}
|
||||||
/* Will never get here (IllegalArgumentException in getConfigType) */
|
/* Will never get here (IllegalArgumentException in getConfigType) */
|
||||||
return null;
|
return null;
|
||||||
@ -260,25 +260,25 @@ public class Config {
|
|||||||
App app = App.self;
|
App app = App.self;
|
||||||
switch (getConfigType(key)) {
|
switch (getConfigType(key)) {
|
||||||
case PREF_INT:
|
case PREF_INT:
|
||||||
app.prefs.edit().putInt(key, (int) val).apply();
|
app.getPrefs().edit().putInt(key, (int) val).apply();
|
||||||
break;
|
break;
|
||||||
case PREF_STR_INT:
|
case PREF_STR_INT:
|
||||||
app.prefs.edit().putString(key, String.valueOf(val)).apply();
|
app.getPrefs().edit().putString(key, String.valueOf(val)).apply();
|
||||||
break;
|
break;
|
||||||
case PREF_BOOL:
|
case PREF_BOOL:
|
||||||
app.prefs.edit().putBoolean(key, (boolean) val).apply();
|
app.getPrefs().edit().putBoolean(key, (boolean) val).apply();
|
||||||
break;
|
break;
|
||||||
case PREF_STR:
|
case PREF_STR:
|
||||||
app.prefs.edit().putString(key, (String) val).apply();
|
app.getPrefs().edit().putString(key, (String) val).apply();
|
||||||
break;
|
break;
|
||||||
case DB_INT:
|
case DB_INT:
|
||||||
app.mDB.setSettings(key, (int) val);
|
app.getDB().setSettings(key, (int) val);
|
||||||
break;
|
break;
|
||||||
case DB_BOOL:
|
case DB_BOOL:
|
||||||
app.mDB.setSettings(key, (boolean) val ? 1 : 0);
|
app.getDB().setSettings(key, (boolean) val ? 1 : 0);
|
||||||
break;
|
break;
|
||||||
case DB_STR:
|
case DB_STR:
|
||||||
app.mDB.setStrings(key, (String) val);
|
app.getDB().setStrings(key, (String) val);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -290,14 +290,14 @@ public class Config {
|
|||||||
case PREF_STR_INT:
|
case PREF_STR_INT:
|
||||||
case PREF_BOOL:
|
case PREF_BOOL:
|
||||||
case PREF_STR:
|
case PREF_STR:
|
||||||
app.prefs.edit().remove(key).apply();
|
app.getPrefs().edit().remove(key).apply();
|
||||||
break;
|
break;
|
||||||
case DB_BOOL:
|
case DB_BOOL:
|
||||||
case DB_INT:
|
case DB_INT:
|
||||||
app.mDB.rmSettings(key);
|
app.getDB().rmSettings(key);
|
||||||
break;
|
break;
|
||||||
case DB_STR:
|
case DB_STR:
|
||||||
app.mDB.setStrings(key, null);
|
app.getDB().setStrings(key, null);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -365,13 +365,13 @@ public class Config {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case DB_INT:
|
case DB_INT:
|
||||||
editor.putString(key, String.valueOf(
|
editor.putString(key, String.valueOf(
|
||||||
app.mDB.getSettings(key, (Integer) defs.get(key))));
|
app.getDB().getSettings(key, (Integer) defs.get(key))));
|
||||||
continue;
|
continue;
|
||||||
case DB_STR:
|
case DB_STR:
|
||||||
editor.putString(key, app.mDB.getStrings(key, (String) defs.get(key)));
|
editor.putString(key, app.getDB().getStrings(key, (String) defs.get(key)));
|
||||||
continue;
|
continue;
|
||||||
case DB_BOOL:
|
case DB_BOOL:
|
||||||
int bs = app.mDB.getSettings(key, -1);
|
int bs = app.getDB().getSettings(key, -1);
|
||||||
editor.putBoolean(key, bs < 0 ? (Boolean) defs.get(key) : bs != 0);
|
editor.putBoolean(key, bs < 0 ? (Boolean) defs.get(key) : bs != 0);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,9 @@ public class Const {
|
|||||||
|
|
||||||
public static final int USER_ID = Process.myUid() / 100000;
|
public static final int USER_ID = Process.myUid() / 100000;
|
||||||
|
|
||||||
|
// Generic
|
||||||
|
public static final String MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log";
|
||||||
|
|
||||||
public static final class MAGISK_VER {
|
public static final class MAGISK_VER {
|
||||||
public static final int MIN_SUPPORT = 18000;
|
public static final int MIN_SUPPORT = 18000;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
package com.topjohnwu.magisk.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.skoumal.teanity.rxbus.RxBus
|
||||||
|
import com.topjohnwu.magisk.App
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
|
||||||
|
val applicationModule = module {
|
||||||
|
single { RxBus() }
|
||||||
|
single { get<Context>().resources }
|
||||||
|
single { get<Context>() as App }
|
||||||
|
single { get<Context>().packageManager }
|
||||||
|
single(SUTimeout) {
|
||||||
|
get<App>().protectedContext
|
||||||
|
.getSharedPreferences("su_timeout", 0)
|
||||||
|
}
|
||||||
|
single { PreferenceManager.getDefaultSharedPreferences(get<App>().protectedContext) }
|
||||||
|
}
|
12
app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt
Normal file
12
app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package com.topjohnwu.magisk.di
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.App
|
||||||
|
import com.topjohnwu.magisk.data.database.MagiskDB
|
||||||
|
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
|
||||||
|
val databaseModule = module {
|
||||||
|
single { MagiskDB(get<App>().protectedContext) }
|
||||||
|
single { RepoDatabaseHelper(get()) }
|
||||||
|
}
|
10
app/src/main/java/com/topjohnwu/magisk/di/MiscModule.kt
Normal file
10
app/src/main/java/com/topjohnwu/magisk/di/MiscModule.kt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package com.topjohnwu.magisk.di
|
||||||
|
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
|
||||||
|
val miscModule = module {
|
||||||
|
|
||||||
|
// define miscs here
|
||||||
|
|
||||||
|
}
|
10
app/src/main/java/com/topjohnwu/magisk/di/Modules.kt
Normal file
10
app/src/main/java/com/topjohnwu/magisk/di/Modules.kt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package com.topjohnwu.magisk.di
|
||||||
|
|
||||||
|
val koinModules = listOf(
|
||||||
|
applicationModule,
|
||||||
|
networkingModule,
|
||||||
|
databaseModule,
|
||||||
|
repositoryModule,
|
||||||
|
viewModelModules,
|
||||||
|
miscModule
|
||||||
|
)
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.topjohnwu.magisk.di
|
||||||
|
|
||||||
|
import org.koin.core.qualifier.named
|
||||||
|
|
||||||
|
val SUTimeout = named("su_timeout")
|
@ -0,0 +1,6 @@
|
|||||||
|
package com.topjohnwu.magisk.di
|
||||||
|
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
|
||||||
|
val networkingModule = module {}
|
@ -0,0 +1,6 @@
|
|||||||
|
package com.topjohnwu.magisk.di
|
||||||
|
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
|
||||||
|
val repositoryModule = module {}
|
@ -0,0 +1,30 @@
|
|||||||
|
package com.topjohnwu.magisk.di
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
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.log.LogViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
||||||
|
import com.topjohnwu.magisk.ui.surequest._SuRequestViewModel
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
|
||||||
|
val viewModelModules = module {
|
||||||
|
viewModel { MainViewModel() }
|
||||||
|
viewModel { HomeViewModel(get(), get()) }
|
||||||
|
viewModel { SuperuserViewModel(get(), get(), get(), get()) }
|
||||||
|
viewModel { HideViewModel(get(), get()) }
|
||||||
|
viewModel { ModuleViewModel(get(), get()) }
|
||||||
|
viewModel { LogViewModel(get(), get()) }
|
||||||
|
viewModel { (action: String, uri: Uri?) -> FlashViewModel(action, uri, get()) }
|
||||||
|
viewModel { (intent: Intent, action: String?) ->
|
||||||
|
_SuRequestViewModel(intent, action.orEmpty(), get(), get())
|
||||||
|
}
|
||||||
|
viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) }
|
||||||
|
}
|
@ -1,427 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.model.adapters;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
|
||||||
import android.content.pm.ComponentInfo;
|
|
||||||
import android.content.pm.PackageInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.WorkerThread;
|
|
||||||
import androidx.collection.ArraySet;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.buildware.widget.indeterm.IndeterminateCheckBox;
|
|
||||||
import com.topjohnwu.magisk.App;
|
|
||||||
import com.topjohnwu.magisk.Config;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.utils.Event;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.magisk.view.ArrowExpandable;
|
|
||||||
import com.topjohnwu.magisk.view.Expandable;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import java9.util.Comparators;
|
|
||||||
import java9.util.Lists;
|
|
||||||
import java9.util.Objects;
|
|
||||||
import java9.util.Sets;
|
|
||||||
import java9.util.stream.Collectors;
|
|
||||||
import java9.util.stream.Stream;
|
|
||||||
import java9.util.stream.StreamSupport;
|
|
||||||
|
|
||||||
public class ApplicationAdapter extends SectionedAdapter
|
|
||||||
<ApplicationAdapter.AppViewHolder, ApplicationAdapter.ProcessViewHolder> {
|
|
||||||
|
|
||||||
private static final String SAFETYNET_PROCESS = "com.google.android.gms.unstable";
|
|
||||||
private static final String GMS_PACKAGE = "com.google.android.gms";
|
|
||||||
private static boolean old_hide = false;
|
|
||||||
|
|
||||||
/* A list of apps that should not be shown as hide-able */
|
|
||||||
private static final List<String> HIDE_BLACKLIST = Lists.of(
|
|
||||||
App.self.getPackageName(),
|
|
||||||
"android",
|
|
||||||
"com.android.chrome",
|
|
||||||
"com.chrome.beta",
|
|
||||||
"com.chrome.dev",
|
|
||||||
"com.chrome.canary",
|
|
||||||
"com.android.webview",
|
|
||||||
"com.google.android.webview"
|
|
||||||
);
|
|
||||||
private static final List<String> DEFAULT_HIDELIST = Lists.of(
|
|
||||||
SAFETYNET_PROCESS
|
|
||||||
);
|
|
||||||
|
|
||||||
private static int BOTTOM_MARGIN = -1;
|
|
||||||
|
|
||||||
private List<HideAppInfo> fullList, showList;
|
|
||||||
private List<HideTarget> hideList;
|
|
||||||
private PackageManager pm;
|
|
||||||
private boolean showSystem;
|
|
||||||
|
|
||||||
public ApplicationAdapter(Context context) {
|
|
||||||
fullList = showList = Collections.emptyList();
|
|
||||||
hideList = Collections.emptyList();
|
|
||||||
pm = context.getPackageManager();
|
|
||||||
showSystem = Config.get(Config.Key.SHOW_SYSTEM_APP);
|
|
||||||
AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ViewGroup.MarginLayoutParams getMargins(RecyclerView.ViewHolder vh) {
|
|
||||||
return (ViewGroup.MarginLayoutParams) vh.itemView.getLayoutParams();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSectionCount() {
|
|
||||||
return showList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount(int section) {
|
|
||||||
HideAppInfo app = showList.get(section);
|
|
||||||
return app.expanded ? app.processList.size() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AppViewHolder onCreateSectionViewHolder(ViewGroup parent) {
|
|
||||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_hide_app, parent, false);
|
|
||||||
AppViewHolder vh = new AppViewHolder(v);
|
|
||||||
if (BOTTOM_MARGIN < 0)
|
|
||||||
BOTTOM_MARGIN = getMargins(vh).bottomMargin;
|
|
||||||
return vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ProcessViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_hide_process, parent, false);
|
|
||||||
return new ProcessViewHolder(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindSectionViewHolder(AppViewHolder holder, int section) {
|
|
||||||
HideAppInfo app = showList.get(section);
|
|
||||||
holder.app_name.setText(app.name);
|
|
||||||
holder.app_icon.setImageDrawable(app.info.loadIcon(pm));
|
|
||||||
holder.package_name.setText(app.info.packageName);
|
|
||||||
holder.checkBox.setOnStateChangedListener(null);
|
|
||||||
holder.checkBox.setState(app.getState());
|
|
||||||
holder.ex.setExpanded(app.expanded);
|
|
||||||
|
|
||||||
int index = getItemPosition(section, 0);
|
|
||||||
holder.checkBox.setOnStateChangedListener((IndeterminateCheckBox box, @Nullable Boolean status) -> {
|
|
||||||
if (status != null) {
|
|
||||||
setHide(status, app);
|
|
||||||
if (app.expanded)
|
|
||||||
notifyItemRangeChanged(index, app.processList.size());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (app.processList.size() > 1) {
|
|
||||||
holder.arrow.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
holder.trigger.setOnClickListener((v) -> {
|
|
||||||
if (app.expanded) {
|
|
||||||
app.expanded = false;
|
|
||||||
notifyItemRangeRemoved(index, app.processList.size());
|
|
||||||
holder.ex.collapse();
|
|
||||||
} else {
|
|
||||||
app.expanded = true;
|
|
||||||
notifyItemRangeInserted(index, app.processList.size());
|
|
||||||
holder.ex.expand();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
holder.arrow.setVisibility(View.GONE);
|
|
||||||
holder.trigger.setOnClickListener(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindItemViewHolder(ProcessViewHolder holder, int section, int position) {
|
|
||||||
HideAppInfo app = showList.get(section);
|
|
||||||
HideProcessInfo target = app.processList.get(position);
|
|
||||||
holder.process.setText(target.name);
|
|
||||||
holder.checkbox.setOnCheckedChangeListener(null);
|
|
||||||
holder.checkbox.setChecked(target.hidden);
|
|
||||||
holder.checkbox.setOnCheckedChangeListener((v, checked) -> {
|
|
||||||
setHide(checked, app, target);
|
|
||||||
notifyItemChanged(getSectionPosition(section));
|
|
||||||
});
|
|
||||||
getMargins(holder).bottomMargin =
|
|
||||||
position == app.processList.size() - 1 ? BOTTOM_MARGIN : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void filter(String constraint) {
|
|
||||||
AsyncTask.SERIAL_EXECUTOR.execute(() -> {
|
|
||||||
Stream<HideAppInfo> s = StreamSupport.stream(fullList)
|
|
||||||
.filter(this::systemFilter)
|
|
||||||
.filter(t -> nameFilter(t, constraint));
|
|
||||||
UiThreadHandler.run(() -> {
|
|
||||||
showList = s.collect(Collectors.toList());
|
|
||||||
notifyDataSetChanged();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setShowSystem(boolean b) {
|
|
||||||
showSystem = b;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refresh() {
|
|
||||||
AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setHide(boolean add, HideAppInfo app) {
|
|
||||||
if (add) {
|
|
||||||
StreamSupport.stream(app.processList).forEach(p -> setHide(true, app, p));
|
|
||||||
} else {
|
|
||||||
if (StreamSupport.stream(app.processList)
|
|
||||||
.anyMatch(p -> p.name.equals(SAFETYNET_PROCESS))) {
|
|
||||||
StreamSupport.stream(app.processList).forEach(p -> setHide(false, app, p));
|
|
||||||
} else {
|
|
||||||
// Quick removal
|
|
||||||
Shell.su("magiskhide --rm " + app.info.packageName).submit();
|
|
||||||
StreamSupport.stream(app.processList).forEach(p -> p.hidden = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setHide(boolean add, HideAppInfo app, HideProcessInfo process) {
|
|
||||||
// Don't remove SafetyNet
|
|
||||||
if (!add && DEFAULT_HIDELIST.contains(process.name))
|
|
||||||
return;
|
|
||||||
Shell.su(Utils.fmt("magiskhide --%s %s %s", add ? "add" : "rm",
|
|
||||||
app.info.packageName, process.name)).submit();
|
|
||||||
process.hidden = add;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addProcesses(Set<String> set, ComponentInfo[] infos) {
|
|
||||||
if (infos != null)
|
|
||||||
for (ComponentInfo info : infos)
|
|
||||||
set.add(info.processName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PackageInfo getPackageInfo(String pkg) {
|
|
||||||
// Try super hard to get as much info as possible
|
|
||||||
try {
|
|
||||||
return pm.getPackageInfo(pkg,
|
|
||||||
PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES |
|
|
||||||
PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS);
|
|
||||||
} catch (Exception e1) {
|
|
||||||
try {
|
|
||||||
PackageInfo info = pm.getPackageInfo(pkg, PackageManager.GET_ACTIVITIES);
|
|
||||||
info.services = pm.getPackageInfo(pkg, PackageManager.GET_SERVICES).services;
|
|
||||||
info.receivers = pm.getPackageInfo(pkg, PackageManager.GET_RECEIVERS).receivers;
|
|
||||||
info.providers = pm.getPackageInfo(pkg, PackageManager.GET_PROVIDERS).providers;
|
|
||||||
return info;
|
|
||||||
} catch (Exception e2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
private void loadApps() {
|
|
||||||
hideList = StreamSupport.stream(Shell.su("magiskhide --ls").exec().getOut())
|
|
||||||
.map(HideTarget::new)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
fullList = StreamSupport.stream(pm.getInstalledApplications(0))
|
|
||||||
.filter(info -> !HIDE_BLACKLIST.contains(info.packageName) && info.enabled)
|
|
||||||
.map(info -> {
|
|
||||||
if (old_hide) {
|
|
||||||
return new HideAppInfo(info, Sets.of(info.packageName));
|
|
||||||
} else {
|
|
||||||
Set<String> set = new ArraySet<>();
|
|
||||||
PackageInfo pkg = getPackageInfo(info.packageName);
|
|
||||||
if (pkg != null) {
|
|
||||||
addProcesses(set, pkg.activities);
|
|
||||||
addProcesses(set, pkg.services);
|
|
||||||
addProcesses(set, pkg.receivers);
|
|
||||||
addProcesses(set, pkg.providers);
|
|
||||||
}
|
|
||||||
if (set.isEmpty())
|
|
||||||
return null;
|
|
||||||
return new HideAppInfo(info, set);
|
|
||||||
}
|
|
||||||
}).filter(Objects::nonNull).sorted()
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
Event.trigger(false, Event.MAGISK_HIDE_DONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// True if not system app or user already hidden it
|
|
||||||
private boolean systemFilter(HideAppInfo target) {
|
|
||||||
return showSystem || target.haveHidden() ||
|
|
||||||
(target.info.flags & ApplicationInfo.FLAG_SYSTEM) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean contains(String s, String filter) {
|
|
||||||
return s.toLowerCase().contains(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean nameFilter(HideAppInfo target, String filter) {
|
|
||||||
if (filter == null || filter.isEmpty())
|
|
||||||
return true;
|
|
||||||
filter = filter.toLowerCase();
|
|
||||||
if (contains(target.name, filter))
|
|
||||||
return true;
|
|
||||||
for (HideProcessInfo p : target.processList) {
|
|
||||||
if (contains(p.name, filter))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return contains(target.info.packageName, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
class HideAppInfo implements Comparable<HideAppInfo> {
|
|
||||||
String name;
|
|
||||||
ApplicationInfo info;
|
|
||||||
List<HideProcessInfo> processList;
|
|
||||||
boolean expanded;
|
|
||||||
IndeterminateCheckBox.OnStateChangedListener listener;
|
|
||||||
|
|
||||||
HideAppInfo(ApplicationInfo appInfo, Set<String> set) {
|
|
||||||
info = appInfo;
|
|
||||||
name = Utils.getAppLabel(info, pm);
|
|
||||||
expanded = false;
|
|
||||||
processList = StreamSupport.stream(set)
|
|
||||||
.map(process -> new HideProcessInfo(info.packageName, process))
|
|
||||||
.sorted().collect(Collectors.toList());
|
|
||||||
listener = (IndeterminateCheckBox box, @Nullable Boolean status) -> {
|
|
||||||
if (status != null) {
|
|
||||||
for (HideProcessInfo p : processList) {
|
|
||||||
String cmd = Utils.fmt("magiskhide --%s %s %s",
|
|
||||||
status ? "add" : "rm", info.packageName, p.name);
|
|
||||||
Shell.su(cmd).submit();
|
|
||||||
p.hidden = status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(HideAppInfo o) {
|
|
||||||
Comparator<HideAppInfo> c;
|
|
||||||
c = Comparators.comparing(HideAppInfo::haveHidden);
|
|
||||||
c = Comparators.reversed(c);
|
|
||||||
c = Comparators.thenComparing(c, t -> t.name, String::compareToIgnoreCase);
|
|
||||||
c = Comparators.thenComparing(c, t -> t.info.packageName);
|
|
||||||
return c.compare(this, o);
|
|
||||||
}
|
|
||||||
|
|
||||||
Boolean getState() {
|
|
||||||
boolean all = true;
|
|
||||||
boolean hidden = false;
|
|
||||||
for (HideProcessInfo p : processList) {
|
|
||||||
if (!p.hidden)
|
|
||||||
all = false;
|
|
||||||
else
|
|
||||||
hidden = true;
|
|
||||||
}
|
|
||||||
if (all)
|
|
||||||
return true;
|
|
||||||
return hidden ? null : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean haveHidden() {
|
|
||||||
Boolean c = getState();
|
|
||||||
return c == null ? true : c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class HideProcessInfo implements Comparable<HideProcessInfo> {
|
|
||||||
String name;
|
|
||||||
boolean hidden;
|
|
||||||
|
|
||||||
HideProcessInfo(String pkg, String process) {
|
|
||||||
this.name = process;
|
|
||||||
for (HideTarget t : hideList) {
|
|
||||||
if (t.pkg.equals(pkg) && t.process.equals(process)) {
|
|
||||||
hidden = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(HideProcessInfo o) {
|
|
||||||
Comparator<HideProcessInfo> c;
|
|
||||||
c = Comparators.comparing((HideProcessInfo t) -> t.hidden);
|
|
||||||
c = Comparators.reversed(c);
|
|
||||||
c = Comparators.thenComparing(c, t -> t.name);
|
|
||||||
return c.compare(this, o);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class HideTarget {
|
|
||||||
String pkg;
|
|
||||||
String process;
|
|
||||||
|
|
||||||
HideTarget(String line) {
|
|
||||||
String[] split = line.split("\\|", 2);
|
|
||||||
pkg = split[0];
|
|
||||||
if (split.length == 2) {
|
|
||||||
process = split[1];
|
|
||||||
} else {
|
|
||||||
// Backwards compatibility
|
|
||||||
old_hide = true;
|
|
||||||
process = pkg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
@BindView(R.id.app_icon) ImageView app_icon;
|
|
||||||
@BindView(R.id.app_name) TextView app_name;
|
|
||||||
@BindView(R.id.package_name) TextView package_name;
|
|
||||||
@BindView(R.id.checkbox) IndeterminateCheckBox checkBox;
|
|
||||||
@BindView(R.id.trigger) View trigger;
|
|
||||||
@BindView(R.id.arrow) ImageView arrow;
|
|
||||||
|
|
||||||
Expandable ex;
|
|
||||||
|
|
||||||
AppViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new ApplicationAdapter$AppViewHolder_ViewBinding(this, itemView);
|
|
||||||
ex = new ArrowExpandable(new Expandable() {
|
|
||||||
@Override
|
|
||||||
protected void onExpand() {
|
|
||||||
getMargins(AppViewHolder.this).bottomMargin = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCollapse() {
|
|
||||||
getMargins(AppViewHolder.this).bottomMargin = BOTTOM_MARGIN;
|
|
||||||
}
|
|
||||||
}, arrow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ProcessViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
@BindView(R.id.process) TextView process;
|
|
||||||
@BindView(R.id.checkbox) CheckBox checkbox;
|
|
||||||
|
|
||||||
ProcessViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new ApplicationAdapter$ProcessViewHolder_ViewBinding(this, itemView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,127 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.model.adapters;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.model.entity.Module;
|
|
||||||
import com.topjohnwu.magisk.view.SnackbarMaker;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHolder> {
|
|
||||||
|
|
||||||
private final List<Module> mList;
|
|
||||||
|
|
||||||
public ModulesAdapter(List<Module> list) {
|
|
||||||
mList = list;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_module, parent, false);
|
|
||||||
return new ViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(final ViewHolder holder, int position) {
|
|
||||||
Context context = holder.itemView.getContext();
|
|
||||||
final Module module = mList.get(position);
|
|
||||||
|
|
||||||
String version = module.getVersion();
|
|
||||||
String author = module.getAuthor();
|
|
||||||
String description = module.getDescription();
|
|
||||||
String noInfo = context.getString(R.string.no_info_provided);
|
|
||||||
|
|
||||||
holder.title.setText(module.getName());
|
|
||||||
holder.versionName.setText(TextUtils.isEmpty(version) ? noInfo : version);
|
|
||||||
holder.author.setText(TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author));
|
|
||||||
holder.description.setText(TextUtils.isEmpty(description) ? noInfo : description);
|
|
||||||
|
|
||||||
holder.checkBox.setOnCheckedChangeListener(null);
|
|
||||||
holder.checkBox.setChecked(module.isEnabled());
|
|
||||||
holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> {
|
|
||||||
int snack;
|
|
||||||
if (isChecked) {
|
|
||||||
module.removeDisableFile();
|
|
||||||
snack = R.string.disable_file_removed;
|
|
||||||
} else {
|
|
||||||
module.createDisableFile();
|
|
||||||
snack = R.string.disable_file_created;
|
|
||||||
}
|
|
||||||
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
|
|
||||||
});
|
|
||||||
|
|
||||||
holder.delete.setOnClickListener(v -> {
|
|
||||||
boolean removed = module.willBeRemoved();
|
|
||||||
int snack;
|
|
||||||
if (removed) {
|
|
||||||
module.deleteRemoveFile();
|
|
||||||
snack = R.string.remove_file_deleted;
|
|
||||||
} else {
|
|
||||||
module.createRemoveFile();
|
|
||||||
snack = R.string.remove_file_created;
|
|
||||||
}
|
|
||||||
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
|
|
||||||
updateDeleteButton(holder, module);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (module.isUpdated()) {
|
|
||||||
holder.notice.setVisibility(View.VISIBLE);
|
|
||||||
holder.notice.setText(R.string.update_file_created);
|
|
||||||
holder.delete.setEnabled(false);
|
|
||||||
} else {
|
|
||||||
updateDeleteButton(holder, module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateDeleteButton(ViewHolder holder, Module module) {
|
|
||||||
holder.notice.setVisibility(module.willBeRemoved() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
if (module.willBeRemoved()) {
|
|
||||||
holder.delete.setImageResource(R.drawable.ic_undelete);
|
|
||||||
} else {
|
|
||||||
holder.delete.setImageResource(R.drawable.ic_delete);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return mList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
@BindView(R.id.title) TextView title;
|
|
||||||
@BindView(R.id.version_name) TextView versionName;
|
|
||||||
@BindView(R.id.description) TextView description;
|
|
||||||
@BindView(R.id.notice) TextView notice;
|
|
||||||
@BindView(R.id.checkbox) CheckBox checkBox;
|
|
||||||
@BindView(R.id.author) TextView author;
|
|
||||||
@BindView(R.id.delete) ImageView delete;
|
|
||||||
|
|
||||||
ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new ModulesAdapter$ViewHolder_ViewBinding(this, itemView);
|
|
||||||
|
|
||||||
if (!Shell.rootAccess()) {
|
|
||||||
checkBox.setEnabled(false);
|
|
||||||
delete.setEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,172 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.model.adapters;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.widget.SwitchCompat;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.data.database.MagiskDB;
|
|
||||||
import com.topjohnwu.magisk.model.entity.Policy;
|
|
||||||
import com.topjohnwu.magisk.utils.FingerprintHelper;
|
|
||||||
import com.topjohnwu.magisk.view.ArrowExpandable;
|
|
||||||
import com.topjohnwu.magisk.view.Expandable;
|
|
||||||
import com.topjohnwu.magisk.view.ExpandableViewHolder;
|
|
||||||
import com.topjohnwu.magisk.view.SnackbarMaker;
|
|
||||||
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog;
|
|
||||||
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class PolicyAdapter extends RecyclerView.Adapter<PolicyAdapter.ViewHolder> {
|
|
||||||
|
|
||||||
private List<Policy> policyList;
|
|
||||||
private MagiskDB dbHelper;
|
|
||||||
private PackageManager pm;
|
|
||||||
private boolean[] expandList;
|
|
||||||
|
|
||||||
public PolicyAdapter(List<Policy> list, MagiskDB db, PackageManager pm) {
|
|
||||||
policyList = list;
|
|
||||||
expandList = new boolean[policyList.size()];
|
|
||||||
dbHelper = db;
|
|
||||||
this.pm = pm;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_policy, parent, false);
|
|
||||||
return new ViewHolder(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
|
||||||
Policy policy = policyList.get(position);
|
|
||||||
|
|
||||||
holder.settings.setExpanded(expandList[position]);
|
|
||||||
holder.trigger.setOnClickListener(view -> {
|
|
||||||
if (holder.settings.isExpanded()) {
|
|
||||||
holder.settings.collapse();
|
|
||||||
expandList[position] = false;
|
|
||||||
} else {
|
|
||||||
holder.settings.expand();
|
|
||||||
expandList[position] = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
holder.appName.setText(policy.appName);
|
|
||||||
holder.packageName.setText(policy.packageName);
|
|
||||||
holder.appIcon.setImageDrawable(policy.info.loadIcon(pm));
|
|
||||||
|
|
||||||
holder.notificationSwitch.setOnCheckedChangeListener(null);
|
|
||||||
holder.loggingSwitch.setOnCheckedChangeListener(null);
|
|
||||||
|
|
||||||
holder.masterSwitch.setChecked(policy.policy == Policy.ALLOW);
|
|
||||||
holder.notificationSwitch.setChecked(policy.notification);
|
|
||||||
holder.loggingSwitch.setChecked(policy.logging);
|
|
||||||
|
|
||||||
holder.masterSwitch.setOnClickListener(v -> {
|
|
||||||
boolean isChecked = holder.masterSwitch.isChecked();
|
|
||||||
Runnable r = () -> {
|
|
||||||
if ((isChecked && policy.policy == Policy.DENY) ||
|
|
||||||
(!isChecked && policy.policy == Policy.ALLOW)) {
|
|
||||||
policy.policy = isChecked ? Policy.ALLOW : Policy.DENY;
|
|
||||||
String message = v.getContext().getString(
|
|
||||||
isChecked ? R.string.su_snack_grant : R.string.su_snack_deny, policy.appName);
|
|
||||||
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
|
||||||
dbHelper.updatePolicy(policy);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (FingerprintHelper.useFingerprint()) {
|
|
||||||
holder.masterSwitch.setChecked(!isChecked);
|
|
||||||
new FingerprintAuthDialog((Activity) v.getContext(), () -> {
|
|
||||||
holder.masterSwitch.setChecked(isChecked);
|
|
||||||
r.run();
|
|
||||||
}).show();
|
|
||||||
} else {
|
|
||||||
r.run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
holder.notificationSwitch.setOnCheckedChangeListener((v, isChecked) -> {
|
|
||||||
if ((isChecked && !policy.notification) ||
|
|
||||||
(!isChecked && policy.notification)) {
|
|
||||||
policy.notification = isChecked;
|
|
||||||
String message = v.getContext().getString(
|
|
||||||
isChecked ? R.string.su_snack_notif_on : R.string.su_snack_notif_off, policy.appName);
|
|
||||||
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
|
||||||
dbHelper.updatePolicy(policy);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
holder.loggingSwitch.setOnCheckedChangeListener((v, isChecked) -> {
|
|
||||||
if ((isChecked && !policy.logging) ||
|
|
||||||
(!isChecked && policy.logging)) {
|
|
||||||
policy.logging = isChecked;
|
|
||||||
String message = v.getContext().getString(
|
|
||||||
isChecked ? R.string.su_snack_log_on : R.string.su_snack_log_off, policy.appName);
|
|
||||||
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
|
||||||
dbHelper.updatePolicy(policy);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
holder.delete.setOnClickListener(v -> {
|
|
||||||
DialogInterface.OnClickListener l = (dialog, which) -> {
|
|
||||||
policyList.remove(position);
|
|
||||||
notifyItemRemoved(position);
|
|
||||||
notifyItemRangeChanged(position, policyList.size());
|
|
||||||
SnackbarMaker.make(holder.itemView, v.getContext().getString(R.string.su_snack_revoke, policy.appName),
|
|
||||||
Snackbar.LENGTH_SHORT).show();
|
|
||||||
dbHelper.deletePolicy(policy);
|
|
||||||
};
|
|
||||||
if (FingerprintHelper.useFingerprint()) {
|
|
||||||
new FingerprintAuthDialog((Activity) v.getContext(),
|
|
||||||
() -> l.onClick(null, 0)).show();
|
|
||||||
} else {
|
|
||||||
new CustomAlertDialog((Activity) v.getContext())
|
|
||||||
.setTitle(R.string.su_revoke_title)
|
|
||||||
.setMessage(v.getContext().getString(R.string.su_revoke_msg, policy.appName))
|
|
||||||
.setPositiveButton(R.string.yes, l)
|
|
||||||
.setNegativeButton(R.string.no_thanks, null)
|
|
||||||
.setCancelable(true)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return policyList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
@BindView(R.id.app_name) TextView appName;
|
|
||||||
@BindView(R.id.package_name) TextView packageName;
|
|
||||||
@BindView(R.id.app_icon) ImageView appIcon;
|
|
||||||
@BindView(R.id.master_switch) SwitchCompat masterSwitch;
|
|
||||||
@BindView(R.id.notification_switch) SwitchCompat notificationSwitch;
|
|
||||||
@BindView(R.id.logging_switch) SwitchCompat loggingSwitch;
|
|
||||||
@BindView(R.id.expand_layout) ViewGroup expandLayout;
|
|
||||||
@BindView(R.id.arrow) ImageView arrow;
|
|
||||||
@BindView(R.id.trigger) View trigger;
|
|
||||||
@BindView(R.id.delete) ImageView delete;
|
|
||||||
@BindView(R.id.more_info) ImageView moreInfo;
|
|
||||||
|
|
||||||
Expandable settings;
|
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new PolicyAdapter$ViewHolder_ViewBinding(this, itemView);
|
|
||||||
settings = new ArrowExpandable(new ExpandableViewHolder(expandLayout), arrow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,246 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.model.adapters;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Pair;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.SearchView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.App;
|
|
||||||
import com.topjohnwu.magisk.ClassMap;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
|
|
||||||
import com.topjohnwu.magisk.model.download.DownloadModuleService;
|
|
||||||
import com.topjohnwu.magisk.model.entity.Module;
|
|
||||||
import com.topjohnwu.magisk.model.entity.Repo;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.utils.Event;
|
|
||||||
import com.topjohnwu.magisk.view.MarkDownWindow;
|
|
||||||
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import java9.util.stream.StreamSupport;
|
|
||||||
|
|
||||||
public class ReposAdapter
|
|
||||||
extends SectionedAdapter<ReposAdapter.SectionHolder, ReposAdapter.RepoHolder>
|
|
||||||
implements Event.AutoListener, SearchView.OnQueryTextListener {
|
|
||||||
|
|
||||||
private static final int UPDATES = 0;
|
|
||||||
private static final int INSTALLED = 1;
|
|
||||||
private static final int OTHERS = 2;
|
|
||||||
|
|
||||||
private Map<String, Module> moduleMap;
|
|
||||||
private RepoDatabaseHelper repoDB;
|
|
||||||
private List<Pair<Integer, List<Repo>>> repoPairs;
|
|
||||||
private List<Repo> fullList;
|
|
||||||
private SearchView mSearch;
|
|
||||||
|
|
||||||
public ReposAdapter() {
|
|
||||||
repoDB = App.self.repoDB;
|
|
||||||
moduleMap = Collections.emptyMap();
|
|
||||||
fullList = Collections.emptyList();
|
|
||||||
repoPairs = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSectionCount() {
|
|
||||||
return repoPairs.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount(int section) {
|
|
||||||
return repoPairs.get(section).second.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SectionHolder onCreateSectionViewHolder(ViewGroup parent) {
|
|
||||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.section, parent, false);
|
|
||||||
return new SectionHolder(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RepoHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_repo, parent, false);
|
|
||||||
return new RepoHolder(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindSectionViewHolder(SectionHolder holder, int section) {
|
|
||||||
switch (repoPairs.get(section).first) {
|
|
||||||
case UPDATES:
|
|
||||||
holder.sectionText.setText(R.string.update_available);
|
|
||||||
break;
|
|
||||||
case INSTALLED:
|
|
||||||
holder.sectionText.setText(R.string.installed);
|
|
||||||
break;
|
|
||||||
case OTHERS:
|
|
||||||
holder.sectionText.setText(R.string.not_installed);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindItemViewHolder(RepoHolder holder, int section, int position) {
|
|
||||||
Repo repo = repoPairs.get(section).second.get(position);
|
|
||||||
Context context = holder.itemView.getContext();
|
|
||||||
|
|
||||||
String name = repo.getName();
|
|
||||||
String version = repo.getVersion();
|
|
||||||
String author = repo.getAuthor();
|
|
||||||
String description = repo.getDescription();
|
|
||||||
String noInfo = context.getString(R.string.no_info_provided);
|
|
||||||
|
|
||||||
holder.title.setText(TextUtils.isEmpty(name) ? noInfo : name);
|
|
||||||
holder.versionName.setText(TextUtils.isEmpty(version) ? noInfo : version);
|
|
||||||
holder.author.setText(TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author));
|
|
||||||
holder.description.setText(TextUtils.isEmpty(description) ? noInfo : description);
|
|
||||||
holder.updateTime.setText(context.getString(R.string.updated_on, repo.getLastUpdateString()));
|
|
||||||
|
|
||||||
holder.infoLayout.setOnClickListener(v ->
|
|
||||||
MarkDownWindow.show((BaseActivity) context, null, repo.getDetailUrl()));
|
|
||||||
|
|
||||||
holder.downloadImage.setOnClickListener(v -> {
|
|
||||||
new CustomAlertDialog((BaseActivity) context)
|
|
||||||
.setTitle(context.getString(R.string.repo_install_title, repo.getName()))
|
|
||||||
.setMessage(context.getString(R.string.repo_install_msg, repo.getDownloadFilename()))
|
|
||||||
.setCancelable(true)
|
|
||||||
.setPositiveButton(R.string.install, (d, i) ->
|
|
||||||
startDownload((BaseActivity) context, repo, true))
|
|
||||||
.setNeutralButton(R.string.download, (d, i) ->
|
|
||||||
startDownload((BaseActivity) context, repo, false))
|
|
||||||
.setNegativeButton(R.string.no_thanks, null)
|
|
||||||
.show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startDownload(BaseActivity activity, Repo repo, Boolean install) {
|
|
||||||
activity.runWithExternalRW(() -> {
|
|
||||||
Intent intent = new Intent(activity, ClassMap.get(DownloadModuleService.class))
|
|
||||||
.putExtra("repo", repo).putExtra("install", install);
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
activity.startForegroundService(intent);
|
|
||||||
} else {
|
|
||||||
activity.startService(intent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateLists() {
|
|
||||||
if (mSearch != null)
|
|
||||||
onQueryTextChange(mSearch.getQuery().toString());
|
|
||||||
else
|
|
||||||
onQueryTextChange("");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean noCaseContain(String a, String b) {
|
|
||||||
return a.toLowerCase().contains(b.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSearchView(SearchView view) {
|
|
||||||
mSearch = view;
|
|
||||||
mSearch.setOnQueryTextListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notifyDBChanged(boolean refresh) {
|
|
||||||
try (Cursor c = repoDB.getRepoCursor()) {
|
|
||||||
fullList = new ArrayList<>(c.getCount());
|
|
||||||
while (c.moveToNext())
|
|
||||||
fullList.add(new Repo(c));
|
|
||||||
}
|
|
||||||
if (refresh)
|
|
||||||
updateLists();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEvent(int event) {
|
|
||||||
moduleMap = Event.getResult(event);
|
|
||||||
updateLists();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int[] getListeningEvents() {
|
|
||||||
return new int[] {Event.MODULE_LOAD_DONE};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onQueryTextSubmit(String query) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onQueryTextChange(String s) {
|
|
||||||
List<Repo> updates = new ArrayList<>();
|
|
||||||
List<Repo> installed = new ArrayList<>();
|
|
||||||
List<Repo> others = new ArrayList<>();
|
|
||||||
|
|
||||||
StreamSupport.stream(fullList)
|
|
||||||
.filter(repo -> noCaseContain(repo.getName(), s)
|
|
||||||
|| noCaseContain(repo.getAuthor(), s)
|
|
||||||
|| noCaseContain(repo.getDescription(), s))
|
|
||||||
.forEach(repo -> {
|
|
||||||
Module module = moduleMap.get(repo.getId());
|
|
||||||
if (module != null) {
|
|
||||||
if (repo.getVersionCode() > module.getVersionCode()) {
|
|
||||||
// Updates
|
|
||||||
updates.add(repo);
|
|
||||||
} else {
|
|
||||||
installed.add(repo);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
others.add(repo);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
repoPairs.clear();
|
|
||||||
if (!updates.isEmpty())
|
|
||||||
repoPairs.add(new Pair<>(UPDATES, updates));
|
|
||||||
if (!installed.isEmpty())
|
|
||||||
repoPairs.add(new Pair<>(INSTALLED, installed));
|
|
||||||
if (!others.isEmpty())
|
|
||||||
repoPairs.add(new Pair<>(OTHERS, others));
|
|
||||||
|
|
||||||
notifyDataSetChanged();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static class SectionHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
@BindView(R.id.section_text) TextView sectionText;
|
|
||||||
|
|
||||||
SectionHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new ReposAdapter$SectionHolder_ViewBinding(this, itemView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class RepoHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
@BindView(R.id.title) TextView title;
|
|
||||||
@BindView(R.id.version_name) TextView versionName;
|
|
||||||
@BindView(R.id.description) TextView description;
|
|
||||||
@BindView(R.id.author) TextView author;
|
|
||||||
@BindView(R.id.info_layout) View infoLayout;
|
|
||||||
@BindView(R.id.download) ImageView downloadImage;
|
|
||||||
@BindView(R.id.update_time) TextView updateTime;
|
|
||||||
|
|
||||||
RepoHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new ReposAdapter$RepoHolder_ViewBinding(this, itemView);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.model.adapters;
|
|
||||||
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
public abstract class SectionedAdapter<S extends RecyclerView.ViewHolder, C extends RecyclerView.ViewHolder>
|
|
||||||
extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
|
||||||
|
|
||||||
private static final int SECTION_TYPE = Integer.MIN_VALUE;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
final public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
if (viewType == SECTION_TYPE)
|
|
||||||
return onCreateSectionViewHolder(parent);
|
|
||||||
return onCreateItemViewHolder(parent, viewType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
final public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
|
||||||
PositionInfo info = getPositionInfo(position);
|
|
||||||
if (info.position == -1)
|
|
||||||
onBindSectionViewHolder((S) holder, info.section);
|
|
||||||
else
|
|
||||||
onBindItemViewHolder((C) holder, info.section, info.position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
final public int getItemCount() {
|
|
||||||
int size, sec;
|
|
||||||
size = sec = getSectionCount();
|
|
||||||
for (int i = 0; i < sec; ++i){
|
|
||||||
size += getItemCount(i);
|
|
||||||
}
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
final public int getItemViewType(int position) {
|
|
||||||
PositionInfo info = getPositionInfo(position);
|
|
||||||
if (info.position == -1)
|
|
||||||
return SECTION_TYPE;
|
|
||||||
else
|
|
||||||
return getItemViewType(info.section, info.position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getItemViewType(int section, int position) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int getSectionPosition(int section) {
|
|
||||||
return getItemPosition(section, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int getItemPosition(int section, int position) {
|
|
||||||
int realPosition = 0;
|
|
||||||
// Previous sections
|
|
||||||
for (int i = 0; i < section; ++i) {
|
|
||||||
realPosition += getItemCount(i) + 1;
|
|
||||||
}
|
|
||||||
// Current section
|
|
||||||
realPosition += position + 1;
|
|
||||||
return realPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PositionInfo getPositionInfo(int position) {
|
|
||||||
int section = 0;
|
|
||||||
while (true) {
|
|
||||||
if (position == 0)
|
|
||||||
return new PositionInfo(section, -1);
|
|
||||||
position -= 1;
|
|
||||||
if (position < getItemCount(section))
|
|
||||||
return new PositionInfo(section, position);
|
|
||||||
position -= getItemCount(section++);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class PositionInfo {
|
|
||||||
int section;
|
|
||||||
int position;
|
|
||||||
PositionInfo(int section, int position) {
|
|
||||||
this.section = section;
|
|
||||||
this.position = position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract int getSectionCount();
|
|
||||||
public abstract int getItemCount(int section);
|
|
||||||
public abstract S onCreateSectionViewHolder(ViewGroup parent);
|
|
||||||
public abstract C onCreateItemViewHolder(ViewGroup parent, int viewType);
|
|
||||||
public abstract void onBindSectionViewHolder(S holder, int section);
|
|
||||||
public abstract void onBindItemViewHolder(C holder, int section, int position);
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.model.adapters;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.IdRes;
|
|
||||||
import androidx.annotation.LayoutRes;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public abstract class StringListAdapter<VH extends StringListAdapter.ViewHolder>
|
|
||||||
extends RecyclerView.Adapter<VH> {
|
|
||||||
|
|
||||||
private RecyclerView rv;
|
|
||||||
private boolean dynamic;
|
|
||||||
private int screenWidth;
|
|
||||||
private int txtWidth = -1;
|
|
||||||
private int padding;
|
|
||||||
|
|
||||||
protected List<String> mList;
|
|
||||||
|
|
||||||
public StringListAdapter(List<String> list) {
|
|
||||||
this(list, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public StringListAdapter(List<String> list, boolean isDynamic) {
|
|
||||||
mList = list;
|
|
||||||
dynamic = isDynamic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public final VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
View v = LayoutInflater.from(parent.getContext()).inflate(itemLayoutRes(), parent, false);
|
|
||||||
VH vh = createViewHolder(v);
|
|
||||||
if (txtWidth < 0)
|
|
||||||
onUpdateTextWidth(vh);
|
|
||||||
return vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull VH holder, int position) {
|
|
||||||
holder.txt.setText(mList.get(position));
|
|
||||||
holder.txt.getLayoutParams().width = txtWidth;
|
|
||||||
if (dynamic)
|
|
||||||
onUpdateTextWidth(holder);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onUpdateTextWidth(VH vh) {
|
|
||||||
if (txtWidth < 0) {
|
|
||||||
txtWidth = screenWidth - padding;
|
|
||||||
} else {
|
|
||||||
vh.txt.measure(0, 0);
|
|
||||||
int width = vh.txt.getMeasuredWidth();
|
|
||||||
if (width > txtWidth) {
|
|
||||||
txtWidth = width;
|
|
||||||
vh.txt.getLayoutParams().width = txtWidth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rv.getWidth() != txtWidth + padding)
|
|
||||||
rv.requestLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttachedToRecyclerView(@NonNull RecyclerView rv) {
|
|
||||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
|
||||||
((Activity) rv.getContext()).getWindowManager()
|
|
||||||
.getDefaultDisplay().getMetrics(displayMetrics);
|
|
||||||
screenWidth = displayMetrics.widthPixels;
|
|
||||||
padding = rv.getPaddingStart() + rv.getPaddingEnd();
|
|
||||||
this.rv = rv;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final int getItemCount() {
|
|
||||||
return mList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@LayoutRes
|
|
||||||
protected abstract int itemLayoutRes();
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public abstract VH createViewHolder(@NonNull View v);
|
|
||||||
|
|
||||||
public static abstract class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
public TextView txt;
|
|
||||||
|
|
||||||
public ViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
txt = itemView.findViewById(textViewResId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@IdRes
|
|
||||||
protected abstract int textViewResId();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,144 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.model.adapters;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.animation.Animation;
|
|
||||||
import android.view.animation.RotateAnimation;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.data.database.MagiskDB;
|
|
||||||
import com.topjohnwu.magisk.model.entity.SuLogEntry;
|
|
||||||
import com.topjohnwu.magisk.view.ExpandableViewHolder;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class SuLogAdapter extends SectionedAdapter<SuLogAdapter.SectionHolder, SuLogAdapter.LogViewHolder> {
|
|
||||||
|
|
||||||
private List<List<SuLogEntry>> logEntries;
|
|
||||||
private Set<Integer> itemExpanded, sectionExpanded;
|
|
||||||
private MagiskDB suDB;
|
|
||||||
|
|
||||||
public SuLogAdapter(MagiskDB db) {
|
|
||||||
suDB = db;
|
|
||||||
logEntries = Collections.emptyList();
|
|
||||||
sectionExpanded = new HashSet<>();
|
|
||||||
itemExpanded = new HashSet<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSectionCount() {
|
|
||||||
return logEntries.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount(int section) {
|
|
||||||
return sectionExpanded.contains(section) ? logEntries.get(section).size() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SectionHolder onCreateSectionViewHolder(ViewGroup parent) {
|
|
||||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog_group, parent, false);
|
|
||||||
return new SectionHolder(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LogViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog, parent, false);
|
|
||||||
return new LogViewHolder(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindSectionViewHolder(SectionHolder holder, int section) {
|
|
||||||
SuLogEntry entry = logEntries.get(section).get(0);
|
|
||||||
holder.arrow.setRotation(sectionExpanded.contains(section) ? 180 : 0);
|
|
||||||
holder.itemView.setOnClickListener(v -> {
|
|
||||||
RotateAnimation rotate;
|
|
||||||
if (sectionExpanded.contains(section)) {
|
|
||||||
holder.arrow.setRotation(0);
|
|
||||||
rotate = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
|
||||||
sectionExpanded.remove(section);
|
|
||||||
notifyItemRangeRemoved(getItemPosition(section, 0), logEntries.get(section).size());
|
|
||||||
} else {
|
|
||||||
rotate = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
|
||||||
sectionExpanded.add(section);
|
|
||||||
notifyItemRangeInserted(getItemPosition(section, 0), logEntries.get(section).size());
|
|
||||||
}
|
|
||||||
rotate.setDuration(300);
|
|
||||||
rotate.setFillAfter(true);
|
|
||||||
holder.arrow.setAnimation(rotate);
|
|
||||||
});
|
|
||||||
holder.date.setText(entry.getDateString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindItemViewHolder(LogViewHolder holder, int section, int position) {
|
|
||||||
SuLogEntry entry = logEntries.get(section).get(position);
|
|
||||||
int realIdx = getItemPosition(section, position);
|
|
||||||
holder.expandable.setExpanded(itemExpanded.contains(realIdx));
|
|
||||||
holder.itemView.setOnClickListener(view -> {
|
|
||||||
if (holder.expandable.isExpanded()) {
|
|
||||||
holder.expandable.collapse();
|
|
||||||
itemExpanded.remove(realIdx);
|
|
||||||
} else {
|
|
||||||
holder.expandable.expand();
|
|
||||||
itemExpanded.add(realIdx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Context context = holder.itemView.getContext();
|
|
||||||
holder.appName.setText(entry.appName);
|
|
||||||
holder.action.setText(entry.action ? R.string.grant : R.string.deny);
|
|
||||||
holder.pid.setText(context.getString(R.string.pid, entry.fromPid));
|
|
||||||
holder.uid.setText(context.getString(R.string.target_uid, entry.toUid));
|
|
||||||
holder.command.setText(context.getString(R.string.command, entry.command));
|
|
||||||
holder.time.setText(entry.getTimeString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notifyDBChanged() {
|
|
||||||
logEntries = suDB.getLogs();
|
|
||||||
itemExpanded.clear();
|
|
||||||
sectionExpanded.clear();
|
|
||||||
sectionExpanded.add(0);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class SectionHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
@BindView(R.id.date) TextView date;
|
|
||||||
@BindView(R.id.arrow) ImageView arrow;
|
|
||||||
|
|
||||||
SectionHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new SuLogAdapter$SectionHolder_ViewBinding(this, itemView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class LogViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
@BindView(R.id.app_name) TextView appName;
|
|
||||||
@BindView(R.id.action) TextView action;
|
|
||||||
@BindView(R.id.time) TextView time;
|
|
||||||
@BindView(R.id.pid) TextView pid;
|
|
||||||
@BindView(R.id.uid) TextView uid;
|
|
||||||
@BindView(R.id.cmd) TextView command;
|
|
||||||
@BindView(R.id.expand_layout) ViewGroup expandLayout;
|
|
||||||
|
|
||||||
ExpandableViewHolder expandable;
|
|
||||||
|
|
||||||
LogViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
new SuLogAdapter$LogViewHolder_ViewBinding(this, itemView);
|
|
||||||
expandable = new ExpandableViewHolder(expandLayout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.model.adapters;
|
|
||||||
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.fragment.app.FragmentPagerAdapter;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class TabFragmentAdapter extends FragmentPagerAdapter {
|
|
||||||
|
|
||||||
private List<Fragment> fragmentList;
|
|
||||||
private List<String> titleList;
|
|
||||||
|
|
||||||
public TabFragmentAdapter(FragmentManager fm) {
|
|
||||||
super(fm);
|
|
||||||
fragmentList = new ArrayList<>();
|
|
||||||
titleList = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Fragment getItem(int position) {
|
|
||||||
return fragmentList.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return fragmentList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CharSequence getPageTitle(int position) {
|
|
||||||
return titleList.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addTab(Fragment fragment, String title) {
|
|
||||||
fragmentList.add(fragment);
|
|
||||||
titleList.add(title);
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,8 +6,6 @@ import android.content.Intent;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.App;
|
import com.topjohnwu.magisk.App;
|
||||||
import com.topjohnwu.magisk.ClassMap;
|
import com.topjohnwu.magisk.ClassMap;
|
||||||
import com.topjohnwu.magisk.Const;
|
import com.topjohnwu.magisk.Const;
|
||||||
@ -31,6 +29,8 @@ import java.util.zip.ZipEntry;
|
|||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
public class DownloadModuleService extends Service {
|
public class DownloadModuleService extends Service {
|
||||||
|
|
||||||
private List<ProgressNotification> notifications;
|
private List<ProgressNotification> notifications;
|
||||||
|
@ -5,10 +5,10 @@ import android.database.Cursor;
|
|||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
|
public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
|
||||||
|
|
||||||
private String mId, mName, mVersion, mAuthor, mDescription;
|
private String mId, mName, mVersion, mAuthor, mDescription;
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.topjohnwu.magisk.model.entity
|
||||||
|
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import com.topjohnwu.magisk.utils.packageInfo
|
||||||
|
import com.topjohnwu.magisk.utils.processes
|
||||||
|
|
||||||
|
class HideAppInfo(
|
||||||
|
val info: ApplicationInfo,
|
||||||
|
val name: String,
|
||||||
|
val icon: Drawable
|
||||||
|
) {
|
||||||
|
|
||||||
|
val processes = info.packageInfo?.processes?.distinct() ?: listOf(info.packageName)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.topjohnwu.magisk.model.entity
|
||||||
|
|
||||||
|
class HideTarget(line: String) {
|
||||||
|
|
||||||
|
private val split = line.split(Regex("\\|"), 2)
|
||||||
|
|
||||||
|
val packageName = split[0]
|
||||||
|
val process = split.getOrElse(1) { packageName }
|
||||||
|
|
||||||
|
}
|
@ -4,10 +4,10 @@ import android.content.ContentValues;
|
|||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
|
||||||
public class Policy implements Comparable<Policy>{
|
public class Policy implements Comparable<Policy>{
|
||||||
public static final int INTERACTIVE = 0;
|
public static final int INTERACTIVE = 0;
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.topjohnwu.magisk.model.entity.recycler
|
||||||
|
|
||||||
|
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
|
||||||
|
class ConsoleRvItem(val item: String) : ComparableRvItem<ConsoleRvItem>() {
|
||||||
|
override val layoutRes: Int = R.layout.item_console
|
||||||
|
|
||||||
|
override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other)
|
||||||
|
override fun itemSameAs(other: ConsoleRvItem) = item == other.item
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package com.topjohnwu.magisk.model.entity.recycler
|
||||||
|
|
||||||
|
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||||
|
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||||
|
import com.skoumal.teanity.rxbus.RxBus
|
||||||
|
import com.skoumal.teanity.util.DiffObservableList
|
||||||
|
import com.skoumal.teanity.util.KObservableField
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.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.inject
|
||||||
|
import com.topjohnwu.magisk.utils.toggle
|
||||||
|
|
||||||
|
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
|
||||||
|
ComparableRvItem<HideRvItem>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.item_hide_app
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
itemsProcess.forEach {
|
||||||
|
it.isHidden.addOnPropertyChangedCallback { isHiddenState.value = currentState }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 toggleExpansion() {
|
||||||
|
if (items.size <= 1) return
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class HideProcessRvItem(
|
||||||
|
val packageName: String,
|
||||||
|
val process: String,
|
||||||
|
isHidden: Boolean
|
||||||
|
) : ComparableRvItem<HideProcessRvItem>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.item_hide_process
|
||||||
|
|
||||||
|
val isHidden = KObservableField(isHidden)
|
||||||
|
|
||||||
|
private val rxBus: RxBus by inject()
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.isHidden.addOnPropertyChangedCallback {
|
||||||
|
rxBus.post(HideProcessEvent(this@HideProcessRvItem))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggle() = isHidden.toggle()
|
||||||
|
|
||||||
|
override fun contentSameAs(other: HideProcessRvItem): Boolean = itemSameAs(other)
|
||||||
|
override fun itemSameAs(other: HideProcessRvItem): Boolean =
|
||||||
|
packageName == other.packageName && process == other.process
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
package com.topjohnwu.magisk.model.entity.recycler
|
||||||
|
|
||||||
|
import androidx.databinding.ObservableList
|
||||||
|
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||||
|
import com.skoumal.teanity.util.DiffObservableList
|
||||||
|
import com.skoumal.teanity.util.KObservableField
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.model.entity.SuLogEntry
|
||||||
|
import com.topjohnwu.magisk.utils.toggle
|
||||||
|
|
||||||
|
class LogRvItem : ComparableRvItem<LogRvItem>() {
|
||||||
|
override val layoutRes: Int = R.layout.item_page_log
|
||||||
|
|
||||||
|
val items = DiffObservableList(callback)
|
||||||
|
|
||||||
|
fun update(list: List<LogItemRvItem>) {
|
||||||
|
list.firstOrNull()?.isExpanded?.value = true
|
||||||
|
items.update(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
//two of these will never be present, safe to assume it's unique
|
||||||
|
override fun contentSameAs(other: LogRvItem): Boolean = false
|
||||||
|
|
||||||
|
override fun itemSameAs(other: LogRvItem): Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogItemRvItem(
|
||||||
|
val items: ObservableList<ComparableRvItem<*>>
|
||||||
|
) : ComparableRvItem<LogItemRvItem>() {
|
||||||
|
override val layoutRes: Int = R.layout.item_superuser_log
|
||||||
|
|
||||||
|
val date = items.filterIsInstance<LogItemEntryRvItem>().firstOrNull()
|
||||||
|
?.item?.dateString.orEmpty()
|
||||||
|
val isExpanded = KObservableField(false)
|
||||||
|
|
||||||
|
fun toggle() = isExpanded.toggle()
|
||||||
|
|
||||||
|
override fun contentSameAs(other: LogItemRvItem): Boolean = items
|
||||||
|
.any { !other.items.contains(it) }
|
||||||
|
|
||||||
|
override fun itemSameAs(other: LogItemRvItem): Boolean = date == other.date
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogItemEntryRvItem(val item: SuLogEntry) : 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.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.date == other.item.date
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
package com.topjohnwu.magisk.model.entity.recycler
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||||
|
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||||
|
import com.skoumal.teanity.util.KObservableField
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.model.entity.Module
|
||||||
|
import com.topjohnwu.magisk.model.entity.Repo
|
||||||
|
import com.topjohnwu.magisk.utils.get
|
||||||
|
import com.topjohnwu.magisk.utils.toggle
|
||||||
|
|
||||||
|
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.item_module
|
||||||
|
|
||||||
|
val lastActionNotice = KObservableField("")
|
||||||
|
val isChecked = KObservableField(item.isEnabled)
|
||||||
|
val isDeletable = KObservableField(item.willBeRemoved())
|
||||||
|
|
||||||
|
init {
|
||||||
|
isChecked.addOnPropertyChangedCallback {
|
||||||
|
when (it) {
|
||||||
|
true -> item.removeDisableFile().notice(R.string.disable_file_removed)
|
||||||
|
false -> item.createDisableFile().notice(R.string.disable_file_created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isDeletable.addOnPropertyChangedCallback {
|
||||||
|
when (it) {
|
||||||
|
true -> item.createRemoveFile().notice(R.string.remove_file_created)
|
||||||
|
false -> item.deleteRemoveFile().notice(R.string.remove_file_deleted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when {
|
||||||
|
item.isUpdated -> notice(R.string.update_file_created)
|
||||||
|
item.willBeRemoved() -> notice(R.string.remove_file_created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggle() = isChecked.toggle()
|
||||||
|
fun toggleDelete() = isDeletable.toggle()
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
private fun Any.notice(@StringRes info: Int) {
|
||||||
|
lastActionNotice.value = get<Resources>().getString(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contentSameAs(other: ModuleRvItem): Boolean = item.version == other.item.version
|
||||||
|
&& item.versionCode == other.item.versionCode
|
||||||
|
&& item.description == other.item.description
|
||||||
|
|
||||||
|
override fun itemSameAs(other: ModuleRvItem): Boolean = item.name == other.item.name
|
||||||
|
}
|
||||||
|
|
||||||
|
class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.item_repo
|
||||||
|
|
||||||
|
override fun contentSameAs(other: RepoRvItem): Boolean = item.version == other.item.version
|
||||||
|
&& item.lastUpdate == other.item.lastUpdate
|
||||||
|
&& item.versionCode == other.item.versionCode
|
||||||
|
&& item.description == other.item.description
|
||||||
|
|
||||||
|
override fun itemSameAs(other: RepoRvItem): Boolean = item.detailUrl == other.item.detailUrl
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package com.topjohnwu.magisk.model.entity.recycler
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||||
|
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||||
|
import com.skoumal.teanity.rxbus.RxBus
|
||||||
|
import com.skoumal.teanity.util.KObservableField
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.model.entity.Policy
|
||||||
|
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
|
||||||
|
import com.topjohnwu.magisk.utils.inject
|
||||||
|
import com.topjohnwu.magisk.utils.toggle
|
||||||
|
|
||||||
|
class PolicyRvItem(val item: Policy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.item_policy
|
||||||
|
|
||||||
|
val isExpanded = KObservableField(false)
|
||||||
|
val isEnabled = KObservableField(item.policy == Policy.ALLOW)
|
||||||
|
val shouldNotify = KObservableField(item.notification)
|
||||||
|
val shouldLog = KObservableField(item.logging)
|
||||||
|
|
||||||
|
fun toggle() = isExpanded.toggle()
|
||||||
|
|
||||||
|
private val rxBus: RxBus by inject()
|
||||||
|
|
||||||
|
init {
|
||||||
|
isEnabled.addOnPropertyChangedCallback {
|
||||||
|
it ?: return@addOnPropertyChangedCallback
|
||||||
|
rxBus.post(PolicyEnableEvent(this@PolicyRvItem, it))
|
||||||
|
}
|
||||||
|
shouldNotify.addOnPropertyChangedCallback {
|
||||||
|
it ?: return@addOnPropertyChangedCallback
|
||||||
|
item.notification = it
|
||||||
|
rxBus.post(PolicyUpdateEvent.Notification(this@PolicyRvItem))
|
||||||
|
}
|
||||||
|
shouldLog.addOnPropertyChangedCallback {
|
||||||
|
it ?: return@addOnPropertyChangedCallback
|
||||||
|
item.logging = it
|
||||||
|
rxBus.post(PolicyUpdateEvent.Log(this@PolicyRvItem))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contentSameAs(other: PolicyRvItem): Boolean = itemSameAs(other)
|
||||||
|
override fun itemSameAs(other: PolicyRvItem): Boolean = item.uid == other.item.uid
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.topjohnwu.magisk.model.entity.recycler
|
||||||
|
|
||||||
|
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
|
||||||
|
class SectionRvItem(val text: String) : ComparableRvItem<SectionRvItem>() {
|
||||||
|
override val layoutRes: Int = R.layout.item_section
|
||||||
|
|
||||||
|
override fun contentSameAs(other: SectionRvItem) = itemSameAs(other)
|
||||||
|
override fun itemSameAs(other: SectionRvItem) = text == other.text
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.topjohnwu.magisk.model.entity.recycler
|
||||||
|
|
||||||
|
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
|
||||||
|
class SpinnerRvItem(val item: String) : ComparableRvItem<SpinnerRvItem>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.item_spinner
|
||||||
|
|
||||||
|
override fun contentSameAs(other: SpinnerRvItem) = itemSameAs(other)
|
||||||
|
override fun itemSameAs(other: SpinnerRvItem) = item == other.item
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.topjohnwu.magisk.model.entity.state
|
||||||
|
|
||||||
|
enum class IndeterminateState {
|
||||||
|
INDETERMINATE, CHECKED, UNCHECKED
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.topjohnwu.magisk.model.events
|
||||||
|
|
||||||
|
import com.skoumal.teanity.rxbus.RxBus
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
|
||||||
|
|
||||||
|
class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event
|
||||||
|
|
||||||
|
class PolicyEnableEvent(val item: PolicyRvItem, val enable: Boolean) : RxBus.Event
|
||||||
|
sealed class PolicyUpdateEvent(val item: PolicyRvItem) : RxBus.Event {
|
||||||
|
class Notification(item: PolicyRvItem) : PolicyUpdateEvent(item)
|
||||||
|
class Log(item: PolicyRvItem) : PolicyUpdateEvent(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleUpdatedEvent(val item: ModuleRvItem) : RxBus.Event
|
@ -0,0 +1,40 @@
|
|||||||
|
package com.topjohnwu.magisk.model.events
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import com.skoumal.teanity.viewevents.ViewEvent
|
||||||
|
import com.topjohnwu.magisk.model.entity.Policy
|
||||||
|
import com.topjohnwu.magisk.model.entity.Repo
|
||||||
|
import io.reactivex.subjects.PublishSubject
|
||||||
|
|
||||||
|
|
||||||
|
data class OpenLinkEvent(val url: String) : ViewEvent()
|
||||||
|
|
||||||
|
class ManagerInstallEvent : ViewEvent()
|
||||||
|
class MagiskInstallEvent : ViewEvent()
|
||||||
|
|
||||||
|
class ManagerChangelogEvent : ViewEvent()
|
||||||
|
class MagiskChangelogEvent : ViewEvent()
|
||||||
|
|
||||||
|
class UninstallEvent : ViewEvent()
|
||||||
|
class EnvFixEvent : ViewEvent()
|
||||||
|
|
||||||
|
class UpdateSafetyNetEvent : ViewEvent()
|
||||||
|
|
||||||
|
class ViewActionEvent(val action: Activity.() -> Unit) : ViewEvent()
|
||||||
|
|
||||||
|
class OpenFilePickerEvent : ViewEvent()
|
||||||
|
|
||||||
|
class OpenChangelogEvent(val item: Repo) : ViewEvent()
|
||||||
|
class InstallModuleEvent(val item: Repo) : ViewEvent()
|
||||||
|
|
||||||
|
class PageChangedEvent : ViewEvent()
|
||||||
|
|
||||||
|
class PermissionEvent(
|
||||||
|
val permissions: List<String>,
|
||||||
|
val callback: PublishSubject<Boolean>
|
||||||
|
) : ViewEvent()
|
||||||
|
|
||||||
|
class BackPressEvent : ViewEvent()
|
||||||
|
|
||||||
|
class SuDialogEvent(val policy: Policy) : ViewEvent()
|
||||||
|
class DieEvent : ViewEvent()
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.topjohnwu.magisk.model.flash
|
||||||
|
|
||||||
|
interface FlashResultListener {
|
||||||
|
|
||||||
|
fun onResult(isSuccess: Boolean)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
package com.topjohnwu.magisk.model.flash
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.os.postDelayed
|
||||||
|
import com.topjohnwu.magisk.tasks.FlashZip
|
||||||
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
|
import com.topjohnwu.magisk.utils.inject
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
|
|
||||||
|
sealed class Flashing(
|
||||||
|
uri: Uri,
|
||||||
|
private val console: MutableList<String>,
|
||||||
|
log: MutableList<String>,
|
||||||
|
private val resultListener: FlashResultListener
|
||||||
|
) : FlashZip(uri, console, log) {
|
||||||
|
|
||||||
|
override fun onResult(success: Boolean) {
|
||||||
|
if (!success) {
|
||||||
|
console.add("! Installation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
resultListener.onResult(success)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Install(
|
||||||
|
uri: Uri,
|
||||||
|
console: MutableList<String>,
|
||||||
|
log: MutableList<String>,
|
||||||
|
resultListener: FlashResultListener
|
||||||
|
) : Flashing(uri, console, log, resultListener) {
|
||||||
|
|
||||||
|
override fun onResult(success: Boolean) {
|
||||||
|
if (success) {
|
||||||
|
Utils.loadModules()
|
||||||
|
}
|
||||||
|
super.onResult(success)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Uninstall(
|
||||||
|
uri: Uri,
|
||||||
|
console: MutableList<String>,
|
||||||
|
log: MutableList<String>,
|
||||||
|
resultListener: FlashResultListener
|
||||||
|
) : Flashing(uri, console, log, resultListener) {
|
||||||
|
|
||||||
|
private val context: Context by inject()
|
||||||
|
|
||||||
|
override fun onResult(success: Boolean) {
|
||||||
|
if (success) {
|
||||||
|
UiThreadHandler.handler.postDelayed(3000) {
|
||||||
|
Shell.su("pm uninstall " + context.packageName).exec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.onResult(success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package com.topjohnwu.magisk.model.flash
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.topjohnwu.magisk.tasks.MagiskInstaller
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
||||||
|
sealed class Patching(
|
||||||
|
private val console: MutableList<String>,
|
||||||
|
logs: MutableList<String>,
|
||||||
|
private val resultListener: FlashResultListener
|
||||||
|
) : MagiskInstaller(console, logs) {
|
||||||
|
|
||||||
|
override fun onResult(success: Boolean) {
|
||||||
|
if (success) {
|
||||||
|
console.add("- All done!")
|
||||||
|
} else {
|
||||||
|
Shell.sh("rm -rf $installDir").submit()
|
||||||
|
console.add("! Installation failed")
|
||||||
|
}
|
||||||
|
resultListener.onResult(success)
|
||||||
|
}
|
||||||
|
|
||||||
|
class File(
|
||||||
|
private val uri: Uri,
|
||||||
|
console: MutableList<String>,
|
||||||
|
logs: MutableList<String>,
|
||||||
|
resultListener: FlashResultListener
|
||||||
|
) : Patching(console, logs, resultListener) {
|
||||||
|
override fun operations() =
|
||||||
|
extractZip() && handleFile(uri) && patchBoot() && storeBoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SecondSlot(
|
||||||
|
console: MutableList<String>,
|
||||||
|
logs: MutableList<String>,
|
||||||
|
resultListener: FlashResultListener
|
||||||
|
) : Patching(console, logs, resultListener) {
|
||||||
|
override fun operations() =
|
||||||
|
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Direct(
|
||||||
|
console: MutableList<String>,
|
||||||
|
logs: MutableList<String>,
|
||||||
|
resultListener: FlashResultListener
|
||||||
|
) : Patching(console, logs, resultListener) {
|
||||||
|
override fun operations() =
|
||||||
|
findImage() && extractZip() && patchBoot() && flashBoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
package com.topjohnwu.magisk.model.navigation
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.annotation.AnimRes
|
||||||
|
import androidx.annotation.AnimatorRes
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.skoumal.teanity.viewevents.NavigationDslMarker
|
||||||
|
import com.skoumal.teanity.viewevents.ViewEvent
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class MagiskNavigationEvent(
|
||||||
|
val navDirections: MagiskNavDirectionsBuilder,
|
||||||
|
val navOptions: MagiskNavOptions,
|
||||||
|
val animOptions: MagiskAnimBuilder
|
||||||
|
) : ViewEvent() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
operator fun invoke(builder: Builder.() -> Unit) = Builder().apply(builder).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@NavigationDslMarker
|
||||||
|
class Builder {
|
||||||
|
|
||||||
|
private var animOptions: MagiskAnimBuilder = MagiskAnimBuilder()
|
||||||
|
private var navOptions: MagiskNavOptions = MagiskNavOptions()
|
||||||
|
private val directionsBuilder = MagiskNavDirectionsBuilder()
|
||||||
|
|
||||||
|
fun args(builder: Bundle.() -> Unit) = directionsBuilder.args(builder)
|
||||||
|
|
||||||
|
fun navAnim(builder: MagiskAnimBuilder.() -> Unit) {
|
||||||
|
animOptions = MagiskAnimBuilder().apply(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navOptions(builder: MagiskNavOptions.() -> Unit) {
|
||||||
|
navOptions = MagiskNavOptions().apply(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navDirections(builder: MagiskNavDirectionsBuilder.() -> Unit) {
|
||||||
|
directionsBuilder.apply(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun build() = MagiskNavigationEvent(directionsBuilder, navOptions, animOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NavigationDslMarker
|
||||||
|
class MagiskNavDirectionsBuilder {
|
||||||
|
|
||||||
|
var destination: KClass<out Fragment>? = null
|
||||||
|
var isActivity: Boolean = false
|
||||||
|
val args: Bundle = Bundle()
|
||||||
|
|
||||||
|
fun args(builder: Bundle.() -> Unit) = args.apply(builder)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@NavigationDslMarker
|
||||||
|
class MagiskNavOptions {
|
||||||
|
var popUpTo: KClass<*>? = null
|
||||||
|
var inclusive: Boolean = false
|
||||||
|
var clearTask: Boolean = false
|
||||||
|
var singleTop: Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@NavigationDslMarker
|
||||||
|
class MagiskAnimBuilder {
|
||||||
|
@AnimRes
|
||||||
|
@AnimatorRes
|
||||||
|
var enter = 0
|
||||||
|
|
||||||
|
@AnimRes
|
||||||
|
@AnimatorRes
|
||||||
|
var exit = 0
|
||||||
|
|
||||||
|
@AnimRes
|
||||||
|
@AnimatorRes
|
||||||
|
var popEnter = 0
|
||||||
|
|
||||||
|
@AnimRes
|
||||||
|
@AnimatorRes
|
||||||
|
var popExit = 0
|
||||||
|
|
||||||
|
val anySet: Boolean get() = enter != 0 || exit != 0 || popEnter != 0 || popExit != 0
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package com.topjohnwu.magisk.model.navigation
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment
|
||||||
|
import com.topjohnwu.magisk.ui.home.MagiskFragment
|
||||||
|
import com.topjohnwu.magisk.ui.log.LogFragment
|
||||||
|
import com.topjohnwu.magisk.ui.module.ModulesFragment
|
||||||
|
import com.topjohnwu.magisk.ui.module.ReposFragment
|
||||||
|
import com.topjohnwu.magisk.ui.settings.SettingsFragment
|
||||||
|
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment
|
||||||
|
|
||||||
|
|
||||||
|
object Navigation {
|
||||||
|
|
||||||
|
fun home() = MagiskNavigationEvent {
|
||||||
|
navDirections { destination = MagiskFragment::class }
|
||||||
|
navOptions { popUpTo = MagiskFragment::class }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun superuser() = MagiskNavigationEvent {
|
||||||
|
navDirections { destination = SuperuserFragment::class }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun modules() = MagiskNavigationEvent {
|
||||||
|
navDirections { destination = ModulesFragment::class }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun repos() = MagiskNavigationEvent {
|
||||||
|
navDirections { destination = ReposFragment::class }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hide() = MagiskNavigationEvent {
|
||||||
|
navDirections { destination = MagiskHideFragment::class }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun log() = MagiskNavigationEvent {
|
||||||
|
navDirections { destination = LogFragment::class }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun settings() = MagiskNavigationEvent {
|
||||||
|
navDirections { destination = SettingsFragment::class }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromSection(section: String) = when (section) {
|
||||||
|
"superuser" -> superuser()
|
||||||
|
"modules" -> modules()
|
||||||
|
"downloads" -> repos()
|
||||||
|
"magiskhide" -> hide()
|
||||||
|
"log" -> log()
|
||||||
|
"settings" -> settings()
|
||||||
|
else -> home()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object Main {
|
||||||
|
const val OPEN_NAV = 1
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.topjohnwu.magisk.model.navigation
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
interface Navigator {
|
||||||
|
|
||||||
|
//TODO Elevate Fragment to MagiskFragment<*,*> once everything is on board with it
|
||||||
|
val baseFragments: List<KClass<out Fragment>>
|
||||||
|
|
||||||
|
fun navigateTo(event: MagiskNavigationEvent)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package com.topjohnwu.magisk.model.observer
|
||||||
|
|
||||||
|
import androidx.databinding.Observable
|
||||||
|
import androidx.databinding.ObservableField
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
|
||||||
|
class Observer<T>(vararg dependencies: Observable, private val observer: () -> T) :
|
||||||
|
ObservableField<T>(*dependencies), Serializable {
|
||||||
|
|
||||||
|
val value: T get() = observer()
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
message = "Use KObservableField.value syntax from code",
|
||||||
|
replaceWith = ReplaceWith("value")
|
||||||
|
)
|
||||||
|
override fun get(): T {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
message = "Observer cannot be set",
|
||||||
|
level = DeprecationLevel.HIDDEN
|
||||||
|
)
|
||||||
|
override fun set(newValue: T) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Observer(value=$value)"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package com.topjohnwu.magisk.model.permissions
|
||||||
|
|
||||||
|
typealias SimpleCallback = () -> Unit
|
||||||
|
typealias PermissionRationaleCallback = (List<String>) -> Unit
|
||||||
|
|
||||||
|
class PermissionRequestBuilder {
|
||||||
|
|
||||||
|
private var onSuccessCallback: SimpleCallback = {}
|
||||||
|
private var onFailureCallback: SimpleCallback = {}
|
||||||
|
private var onShowRationaleCallback: PermissionRationaleCallback = {}
|
||||||
|
|
||||||
|
fun onSuccess(callback: SimpleCallback) {
|
||||||
|
onSuccessCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onFailure(callback: SimpleCallback) {
|
||||||
|
onFailureCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShowRationale(callback: PermissionRationaleCallback) {
|
||||||
|
onShowRationaleCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): PermissionRequest {
|
||||||
|
return PermissionRequest(onSuccessCallback, onFailureCallback, onShowRationaleCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class PermissionRequest(
|
||||||
|
private val onSuccessCallback: SimpleCallback,
|
||||||
|
private val onFailureCallback: SimpleCallback,
|
||||||
|
private val onShowRationaleCallback: PermissionRationaleCallback
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun onSuccess() = onSuccessCallback()
|
||||||
|
fun onFailure() = onFailureCallback()
|
||||||
|
fun onShowRationale(permissions: List<String>) = onShowRationaleCallback(permissions)
|
||||||
|
|
||||||
|
}
|
@ -61,12 +61,12 @@ public class GeneralReceiver extends BroadcastReceiver {
|
|||||||
case Intent.ACTION_PACKAGE_REPLACED:
|
case Intent.ACTION_PACKAGE_REPLACED:
|
||||||
// This will only work pre-O
|
// This will only work pre-O
|
||||||
if (Config.get(Config.Key.SU_REAUTH)) {
|
if (Config.get(Config.Key.SU_REAUTH)) {
|
||||||
app.mDB.deletePolicy(getPkg(intent));
|
app.getDB().deletePolicy(getPkg(intent));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Intent.ACTION_PACKAGE_FULLY_REMOVED:
|
case Intent.ACTION_PACKAGE_FULLY_REMOVED:
|
||||||
String pkg = getPkg(intent);
|
String pkg = getPkg(intent);
|
||||||
app.mDB.deletePolicy(pkg);
|
app.getDB().deletePolicy(pkg);
|
||||||
Shell.su("magiskhide --rm " + pkg).submit();
|
Shell.su("magiskhide --rm " + pkg).submit();
|
||||||
break;
|
break;
|
||||||
case Intent.ACTION_LOCALE_CHANGED:
|
case Intent.ACTION_LOCALE_CHANGED:
|
||||||
|
@ -4,6 +4,12 @@ import android.content.Context;
|
|||||||
import android.net.Network;
|
import android.net.Network;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import androidx.annotation.MainThread;
|
import androidx.annotation.MainThread;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@ -11,12 +17,6 @@ import androidx.annotation.RequiresApi;
|
|||||||
import androidx.work.Data;
|
import androidx.work.Data;
|
||||||
import androidx.work.ListenableWorker;
|
import androidx.work.ListenableWorker;
|
||||||
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public abstract class DelegateWorker {
|
public abstract class DelegateWorker {
|
||||||
|
|
||||||
private ListenableWorker worker;
|
private ListenableWorker worker;
|
||||||
|
@ -34,7 +34,7 @@ import java.util.concurrent.Future;
|
|||||||
public class UpdateRepos {
|
public class UpdateRepos {
|
||||||
private static final DateFormat DATE_FORMAT;
|
private static final DateFormat DATE_FORMAT;
|
||||||
|
|
||||||
private App app = App.self;
|
private final App app = App.self;
|
||||||
private Set<String> cached;
|
private Set<String> cached;
|
||||||
private Queue<Pair<String, Date>> moduleQueue;
|
private Queue<Pair<String, Date>> moduleQueue;
|
||||||
|
|
||||||
@ -116,17 +116,17 @@ public class UpdateRepos {
|
|||||||
Pair<String, Date> pair = moduleQueue.poll();
|
Pair<String, Date> pair = moduleQueue.poll();
|
||||||
if (pair == null)
|
if (pair == null)
|
||||||
return;
|
return;
|
||||||
Repo repo = app.repoDB.getRepo(pair.first);
|
Repo repo = app.getRepoDB().getRepo(pair.first);
|
||||||
try {
|
try {
|
||||||
if (repo == null)
|
if (repo == null)
|
||||||
repo = new Repo(pair.first);
|
repo = new Repo(pair.first);
|
||||||
else
|
else
|
||||||
cached.remove(pair.first);
|
cached.remove(pair.first);
|
||||||
repo.update(pair.second);
|
repo.update(pair.second);
|
||||||
app.repoDB.addRepo(repo);
|
app.getRepoDB().addRepo(repo);
|
||||||
} catch (Repo.IllegalRepoException e) {
|
} catch (Repo.IllegalRepoException e) {
|
||||||
Logger.debug(e.getMessage());
|
Logger.debug(e.getMessage());
|
||||||
app.repoDB.removeRepo(pair.first);
|
app.getRepoDB().removeRepo(pair.first);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -134,7 +134,7 @@ public class UpdateRepos {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void fullReload() {
|
private void fullReload() {
|
||||||
Cursor c = app.repoDB.getRawCursor();
|
Cursor c = app.getRepoDB().getRawCursor();
|
||||||
runTasks(() -> {
|
runTasks(() -> {
|
||||||
while (true) {
|
while (true) {
|
||||||
Repo repo;
|
Repo repo;
|
||||||
@ -145,10 +145,10 @@ public class UpdateRepos {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
repo.update();
|
repo.update();
|
||||||
app.repoDB.addRepo(repo);
|
app.getRepoDB().addRepo(repo);
|
||||||
} catch (Repo.IllegalRepoException e) {
|
} catch (Repo.IllegalRepoException e) {
|
||||||
Logger.debug(e.getMessage());
|
Logger.debug(e.getMessage());
|
||||||
app.repoDB.removeRepo(repo);
|
app.getRepoDB().removeRepo(repo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -157,12 +157,12 @@ public class UpdateRepos {
|
|||||||
public void exec(boolean force) {
|
public void exec(boolean force) {
|
||||||
Event.reset(Event.REPO_LOAD_DONE);
|
Event.reset(Event.REPO_LOAD_DONE);
|
||||||
App.THREAD_POOL.execute(() -> {
|
App.THREAD_POOL.execute(() -> {
|
||||||
cached = Collections.synchronizedSet(app.repoDB.getRepoIDSet());
|
cached = Collections.synchronizedSet(app.getRepoDB().getRepoIDSet());
|
||||||
moduleQueue = new ConcurrentLinkedQueue<>();
|
moduleQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
if (loadPages()) {
|
if (loadPages()) {
|
||||||
// The leftover cached means they are removed from online repo
|
// The leftover cached means they are removed from online repo
|
||||||
app.repoDB.removeRepo(cached);
|
app.getRepoDB().removeRepo(cached);
|
||||||
} else if (force) {
|
} else if (force) {
|
||||||
fullReload();
|
fullReload();
|
||||||
}
|
}
|
||||||
|
@ -1,194 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
|
||||||
|
|
||||||
import com.google.android.material.navigation.NavigationView;
|
|
||||||
import com.topjohnwu.magisk.ClassMap;
|
|
||||||
import com.topjohnwu.magisk.Config;
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment;
|
|
||||||
import com.topjohnwu.magisk.ui.home.MagiskFragment;
|
|
||||||
import com.topjohnwu.magisk.ui.log.LogFragment;
|
|
||||||
import com.topjohnwu.magisk.ui.module.ModulesFragment;
|
|
||||||
import com.topjohnwu.magisk.ui.module.ReposFragment;
|
|
||||||
import com.topjohnwu.magisk.ui.settings.SettingsFragment;
|
|
||||||
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.net.Networking;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class MainActivity extends BaseActivity
|
|
||||||
implements NavigationView.OnNavigationItemSelectedListener {
|
|
||||||
|
|
||||||
private final Handler mDrawerHandler = new Handler();
|
|
||||||
private int mDrawerItem;
|
|
||||||
private static boolean fromShortcut = false;
|
|
||||||
|
|
||||||
@BindView(R.id.toolbar) public Toolbar toolbar;
|
|
||||||
@BindView(R.id.drawer_layout) DrawerLayout drawer;
|
|
||||||
@BindView(R.id.nav_view) NavigationView navigationView;
|
|
||||||
|
|
||||||
private float toolbarElevation;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getDarkTheme() {
|
|
||||||
return R.style.AppTheme_Dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
|
||||||
if (!SplashActivity.DONE) {
|
|
||||||
startActivity(new Intent(this, ClassMap.get(SplashActivity.class)));
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_main);
|
|
||||||
new MainActivity_ViewBinding(this);
|
|
||||||
checkHideSection();
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
|
|
||||||
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.magisk, R.string.magisk) {
|
|
||||||
@Override
|
|
||||||
public void onDrawerOpened(View drawerView) {
|
|
||||||
super.onDrawerOpened(drawerView);
|
|
||||||
super.onDrawerSlide(drawerView, 0); // this disables the arrow @ completed tate
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDrawerSlide(View drawerView, float slideOffset) {
|
|
||||||
super.onDrawerSlide(drawerView, 0); // this disables the animation
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
toolbarElevation = toolbar.getElevation();
|
|
||||||
}
|
|
||||||
|
|
||||||
drawer.addDrawerListener(toggle);
|
|
||||||
toggle.syncState();
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
String section = getIntent().getStringExtra(Const.Key.OPEN_SECTION);
|
|
||||||
fromShortcut = section != null;
|
|
||||||
navigate(section);
|
|
||||||
}
|
|
||||||
|
|
||||||
navigationView.setNavigationItemSelectedListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
if (drawer.isDrawerOpen(navigationView)) {
|
|
||||||
drawer.closeDrawer(navigationView);
|
|
||||||
} else if (mDrawerItem != R.id.magisk && !fromShortcut) {
|
|
||||||
navigate(R.id.magisk);
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onNavigationItemSelected(@NonNull final MenuItem menuItem) {
|
|
||||||
mDrawerHandler.removeCallbacksAndMessages(null);
|
|
||||||
mDrawerHandler.postDelayed(() -> navigate(menuItem.getItemId()), 250);
|
|
||||||
drawer.closeDrawer(navigationView);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkHideSection() {
|
|
||||||
Menu menu = navigationView.getMenu();
|
|
||||||
menu.findItem(R.id.magiskhide).setVisible(Shell.rootAccess() &&
|
|
||||||
(boolean) Config.get(Config.Key.MAGISKHIDE));
|
|
||||||
menu.findItem(R.id.modules).setVisible(Shell.rootAccess() && Config.magiskVersionCode >= 0);
|
|
||||||
menu.findItem(R.id.downloads).setVisible(Networking.checkNetworkStatus(this)
|
|
||||||
&& Shell.rootAccess() && Config.magiskVersionCode >= 0);
|
|
||||||
menu.findItem(R.id.log).setVisible(Shell.rootAccess());
|
|
||||||
menu.findItem(R.id.superuser).setVisible(Utils.showSuperUser());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void navigate(String item) {
|
|
||||||
int itemId = R.id.magisk;
|
|
||||||
if (item != null) {
|
|
||||||
switch (item) {
|
|
||||||
case "superuser":
|
|
||||||
itemId = R.id.superuser;
|
|
||||||
break;
|
|
||||||
case "modules":
|
|
||||||
itemId = R.id.modules;
|
|
||||||
break;
|
|
||||||
case "downloads":
|
|
||||||
itemId = R.id.downloads;
|
|
||||||
break;
|
|
||||||
case "magiskhide":
|
|
||||||
itemId = R.id.magiskhide;
|
|
||||||
break;
|
|
||||||
case "log":
|
|
||||||
itemId = R.id.log;
|
|
||||||
break;
|
|
||||||
case "settings":
|
|
||||||
itemId = R.id.settings;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
navigate(itemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void navigate(int itemId) {
|
|
||||||
mDrawerItem = itemId;
|
|
||||||
navigationView.setCheckedItem(itemId);
|
|
||||||
switch (itemId) {
|
|
||||||
case R.id.magisk:
|
|
||||||
fromShortcut = false;
|
|
||||||
displayFragment(new MagiskFragment(), true);
|
|
||||||
break;
|
|
||||||
case R.id.superuser:
|
|
||||||
displayFragment(new SuperuserFragment(), true);
|
|
||||||
break;
|
|
||||||
case R.id.modules:
|
|
||||||
displayFragment(new ModulesFragment(), true);
|
|
||||||
break;
|
|
||||||
case R.id.downloads:
|
|
||||||
displayFragment(new ReposFragment(), true);
|
|
||||||
break;
|
|
||||||
case R.id.magiskhide:
|
|
||||||
displayFragment(new MagiskHideFragment(), true);
|
|
||||||
break;
|
|
||||||
case R.id.log:
|
|
||||||
displayFragment(new LogFragment(), false);
|
|
||||||
break;
|
|
||||||
case R.id.settings:
|
|
||||||
displayFragment(new SettingsFragment(), true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displayFragment(@NonNull Fragment navFragment, boolean setElevation) {
|
|
||||||
supportInvalidateOptionsMenu();
|
|
||||||
getSupportFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
|
|
||||||
.replace(R.id.content_frame, navFragment)
|
|
||||||
.commitNow();
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
toolbar.setElevation(setElevation ? toolbarElevation : 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
104
app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt
Normal file
104
app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package com.topjohnwu.magisk.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.core.view.GravityCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.topjohnwu.magisk.ClassMap
|
||||||
|
import com.topjohnwu.magisk.Config
|
||||||
|
import com.topjohnwu.magisk.Const.Key.OPEN_SECTION
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.databinding.ActivityMainBinding
|
||||||
|
import com.topjohnwu.magisk.model.navigation.Navigation
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskActivity
|
||||||
|
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment
|
||||||
|
import com.topjohnwu.magisk.ui.log.LogFragment
|
||||||
|
import com.topjohnwu.magisk.ui.module.ModulesFragment
|
||||||
|
import com.topjohnwu.magisk.ui.module.ReposFragment
|
||||||
|
import com.topjohnwu.magisk.ui.settings.SettingsFragment
|
||||||
|
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment
|
||||||
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
|
import com.topjohnwu.net.Networking
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import com.topjohnwu.magisk.ui.home.MagiskFragment as HomeFragment
|
||||||
|
|
||||||
|
|
||||||
|
open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.activity_main
|
||||||
|
override val viewModel: MainViewModel by viewModel()
|
||||||
|
override val navHostId: Int = R.id.main_nav_host
|
||||||
|
override val defaultPosition: Int = 0
|
||||||
|
|
||||||
|
override val baseFragments: List<KClass<out Fragment>> = listOf(
|
||||||
|
HomeFragment::class,
|
||||||
|
SuperuserFragment::class,
|
||||||
|
MagiskHideFragment::class,
|
||||||
|
ModulesFragment::class,
|
||||||
|
ReposFragment::class,
|
||||||
|
LogFragment::class,
|
||||||
|
SettingsFragment::class
|
||||||
|
)
|
||||||
|
|
||||||
|
/*override fun getDarkTheme(): Int {
|
||||||
|
return R.style.AppTheme_Dark
|
||||||
|
}*/
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
if (!SplashActivity.DONE) {
|
||||||
|
startActivity(Intent(this, ClassMap.get<Any>(SplashActivity::class.java)))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
checkHideSection()
|
||||||
|
setSupportActionBar(binding.mainInclude.mainToolbar)
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
intent.getStringExtra(OPEN_SECTION)?.let {
|
||||||
|
onEventDispatched(Navigation.fromSection(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTitle(title: CharSequence?) {
|
||||||
|
supportActionBar?.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTitle(titleId: Int) {
|
||||||
|
supportActionBar?.setTitle(titleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (binding.drawerLayout.isDrawerOpen(binding.navView)) {
|
||||||
|
binding.drawerLayout.closeDrawer(binding.navView)
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSimpleEventDispatched(event: Int) {
|
||||||
|
super.onSimpleEventDispatched(event)
|
||||||
|
when (event) {
|
||||||
|
Navigation.Main.OPEN_NAV -> openNav()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openNav() = binding.drawerLayout.openDrawer(GravityCompat.START)
|
||||||
|
|
||||||
|
fun checkHideSection() {
|
||||||
|
val menu = binding.navView.menu
|
||||||
|
menu.findItem(R.id.magiskHideFragment).isVisible =
|
||||||
|
Shell.rootAccess() && Config.get<Any>(Config.Key.MAGISKHIDE) as Boolean
|
||||||
|
menu.findItem(R.id.modulesFragment).isVisible =
|
||||||
|
Shell.rootAccess() && Config.magiskVersionCode >= 0
|
||||||
|
menu.findItem(R.id.reposFragment).isVisible =
|
||||||
|
(Networking.checkNetworkStatus(this) && Shell.rootAccess() && Config.magiskVersionCode >= 0)
|
||||||
|
menu.findItem(R.id.logFragment).isVisible =
|
||||||
|
Shell.rootAccess()
|
||||||
|
menu.findItem(R.id.superuserFragment).isVisible =
|
||||||
|
Utils.showSuperUser()
|
||||||
|
}
|
||||||
|
}
|
27
app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt
Normal file
27
app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package com.topjohnwu.magisk.ui
|
||||||
|
|
||||||
|
import android.view.MenuItem
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.model.navigation.Navigation
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||||
|
|
||||||
|
|
||||||
|
class MainViewModel : MagiskViewModel() {
|
||||||
|
|
||||||
|
fun navPressed() = Navigation.Main.OPEN_NAV.publish()
|
||||||
|
|
||||||
|
fun navigationItemPressed(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.magiskFragment -> Navigation.home()
|
||||||
|
R.id.superuserFragment -> Navigation.superuser()
|
||||||
|
R.id.magiskHideFragment -> Navigation.hide()
|
||||||
|
R.id.modulesFragment -> Navigation.modules()
|
||||||
|
R.id.reposFragment -> Navigation.repos()
|
||||||
|
R.id.logFragment -> Navigation.log()
|
||||||
|
R.id.settings -> Navigation.settings()
|
||||||
|
else -> null
|
||||||
|
}?.publish()?.let { return@navigationItemPressed true }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,97 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.BuildConfig;
|
|
||||||
import com.topjohnwu.magisk.ClassMap;
|
|
||||||
import com.topjohnwu.magisk.Config;
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
|
|
||||||
import com.topjohnwu.magisk.tasks.CheckUpdates;
|
|
||||||
import com.topjohnwu.magisk.tasks.UpdateRepos;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.magisk.view.Notifications;
|
|
||||||
import com.topjohnwu.magisk.view.Shortcuts;
|
|
||||||
import com.topjohnwu.net.Networking;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
public class SplashActivity extends BaseActivity {
|
|
||||||
|
|
||||||
public static boolean DONE = false;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
Shell.getShell(shell -> {
|
|
||||||
if (Config.magiskVersionCode > 0 &&
|
|
||||||
Config.magiskVersionCode < Const.MAGISK_VER.MIN_SUPPORT) {
|
|
||||||
new AlertDialog.Builder(this)
|
|
||||||
.setTitle(R.string.unsupport_magisk_title)
|
|
||||||
.setMessage(R.string.unsupport_magisk_message)
|
|
||||||
.setNegativeButton(R.string.ok, null)
|
|
||||||
.setOnDismissListener(dialog -> finish())
|
|
||||||
.show();
|
|
||||||
} else {
|
|
||||||
initAndStart();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initAndStart() {
|
|
||||||
String pkg = Config.get(Config.Key.SU_MANAGER);
|
|
||||||
if (pkg != null && getPackageName().equals(BuildConfig.APPLICATION_ID)) {
|
|
||||||
Config.remove(Config.Key.SU_MANAGER);
|
|
||||||
Shell.su("pm uninstall " + pkg).submit();
|
|
||||||
}
|
|
||||||
if (TextUtils.equals(pkg, getPackageName())) {
|
|
||||||
try {
|
|
||||||
// We are the manager, remove com.topjohnwu.magisk as it could be malware
|
|
||||||
getPackageManager().getApplicationInfo(BuildConfig.APPLICATION_ID, 0);
|
|
||||||
Shell.su("pm uninstall " + BuildConfig.APPLICATION_ID).submit();
|
|
||||||
} catch (PackageManager.NameNotFoundException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic detect all locales
|
|
||||||
LocaleManager.loadAvailableLocales(R.string.app_changelog);
|
|
||||||
|
|
||||||
// Set default configs
|
|
||||||
Config.initialize();
|
|
||||||
|
|
||||||
// Create notification channel on Android O
|
|
||||||
Notifications.setup(this);
|
|
||||||
|
|
||||||
// Schedule periodic update checks
|
|
||||||
Utils.scheduleUpdateCheck();
|
|
||||||
CheckUpdates.check();
|
|
||||||
|
|
||||||
// Setup shortcuts
|
|
||||||
Shortcuts.setup(this);
|
|
||||||
|
|
||||||
// Create repo database
|
|
||||||
app.repoDB = new RepoDatabaseHelper(this);
|
|
||||||
|
|
||||||
// Magisk working as expected
|
|
||||||
if (Shell.rootAccess() && Config.magiskVersionCode > 0) {
|
|
||||||
// Load modules
|
|
||||||
Utils.loadModules(false);
|
|
||||||
// Load repos
|
|
||||||
if (Networking.checkNetworkStatus(this))
|
|
||||||
new UpdateRepos().exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent intent = new Intent(this, ClassMap.get(MainActivity.class));
|
|
||||||
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION));
|
|
||||||
DONE = true;
|
|
||||||
startActivity(intent);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
87
app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt
Normal file
87
app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package com.topjohnwu.magisk.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.TextUtils
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.topjohnwu.magisk.*
|
||||||
|
import com.topjohnwu.magisk.tasks.CheckUpdates
|
||||||
|
import com.topjohnwu.magisk.tasks.UpdateRepos
|
||||||
|
import com.topjohnwu.magisk.utils.LocaleManager
|
||||||
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
|
import com.topjohnwu.net.Networking
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
||||||
|
open class SplashActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
Shell.getShell {
|
||||||
|
if (Config.magiskVersionCode > 0 && Config.magiskVersionCode < Const.MAGISK_VER.MIN_SUPPORT) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.unsupport_magisk_title)
|
||||||
|
.setMessage(R.string.unsupport_magisk_message)
|
||||||
|
.setNegativeButton(R.string.ok, null)
|
||||||
|
.setOnDismissListener { finish() }
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
initAndStart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initAndStart() {
|
||||||
|
val pkg = Config.get<String>(Config.Key.SU_MANAGER)
|
||||||
|
if (pkg != null && packageName == BuildConfig.APPLICATION_ID) {
|
||||||
|
Config.remove(Config.Key.SU_MANAGER)
|
||||||
|
Shell.su("pm uninstall $pkg").submit()
|
||||||
|
}
|
||||||
|
if (TextUtils.equals(pkg, packageName)) {
|
||||||
|
runCatching {
|
||||||
|
// We are the manager, remove com.topjohnwu.magisk as it could be malware
|
||||||
|
packageManager.getApplicationInfo(BuildConfig.APPLICATION_ID, 0)
|
||||||
|
Shell.su("pm uninstall " + BuildConfig.APPLICATION_ID).submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic detect all locales
|
||||||
|
LocaleManager.loadAvailableLocales(R.string.app_changelog)
|
||||||
|
|
||||||
|
// Set default configs
|
||||||
|
Config.initialize()
|
||||||
|
|
||||||
|
// Create notification channel on Android O
|
||||||
|
Notifications.setup(this)
|
||||||
|
|
||||||
|
// Schedule periodic update checks
|
||||||
|
Utils.scheduleUpdateCheck()
|
||||||
|
CheckUpdates.check()
|
||||||
|
|
||||||
|
// Setup shortcuts
|
||||||
|
Shortcuts.setup(this)
|
||||||
|
|
||||||
|
// Magisk working as expected
|
||||||
|
if (Shell.rootAccess() && Config.magiskVersionCode > 0) {
|
||||||
|
// Load modules
|
||||||
|
Utils.loadModules(false)
|
||||||
|
// Load repos
|
||||||
|
if (Networking.checkNetworkStatus(this))
|
||||||
|
UpdateRepos().exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(this, ClassMap.get<Any>(MainActivity::class.java))
|
||||||
|
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION))
|
||||||
|
DONE = true
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
var DONE = false
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.base
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
interface ActivityResultListener {
|
||||||
|
fun onActivityResult(resultCode: Int, data: Intent?)
|
||||||
|
}
|
@ -1,159 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.base;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.ActivityInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.StyleRes;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.collection.SparseArrayCompat;
|
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.App;
|
|
||||||
import com.topjohnwu.magisk.Config;
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.utils.Event;
|
|
||||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
|
||||||
|
|
||||||
public abstract class BaseActivity extends AppCompatActivity implements Event.AutoListener {
|
|
||||||
|
|
||||||
static int[] EMPTY_INT_ARRAY = new int[0];
|
|
||||||
|
|
||||||
private SparseArrayCompat<ActivityResultListener> resultListeners = new SparseArrayCompat<>();
|
|
||||||
public App app = App.self;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int[] getListeningEvents() {
|
|
||||||
return EMPTY_INT_ARRAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEvent(int event) {}
|
|
||||||
|
|
||||||
@StyleRes
|
|
||||||
public int getDarkTheme() {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(Context base) {
|
|
||||||
super.attachBaseContext(LocaleManager.getLocaleContext(base, LocaleManager.locale));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
Event.register(this);
|
|
||||||
if (getDarkTheme() != -1 && (boolean) Config.get(Config.Key.DARK_THEME)) {
|
|
||||||
setTheme(getDarkTheme());
|
|
||||||
}
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
Event.unregister(this);
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setFloating() {
|
|
||||||
boolean isTablet = getResources().getBoolean(R.bool.isTablet);
|
|
||||||
if (isTablet) {
|
|
||||||
WindowManager.LayoutParams params = getWindow().getAttributes();
|
|
||||||
params.height = getResources().getDimensionPixelSize(R.dimen.floating_height);
|
|
||||||
params.width = getResources().getDimensionPixelSize(R.dimen.floating_width);
|
|
||||||
params.alpha = 1.0f;
|
|
||||||
params.dimAmount = 0.6f;
|
|
||||||
params.flags |= 2;
|
|
||||||
getWindow().setAttributes(params);
|
|
||||||
setFinishOnTouchOutside(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void lockOrientation() {
|
|
||||||
if (Build.VERSION.SDK_INT < 18)
|
|
||||||
setRequestedOrientation(getResources().getConfiguration().orientation);
|
|
||||||
else
|
|
||||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void runWithExternalRW(Runnable callback) {
|
|
||||||
runWithPermissions(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void runWithPermissions(String[] permissions, Runnable callback) {
|
|
||||||
runWithPermissions(this, permissions, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void runWithPermissions(Context context, String[] permissions, Runnable callback) {
|
|
||||||
boolean granted = true;
|
|
||||||
for (String perm : permissions) {
|
|
||||||
if (ContextCompat.checkSelfPermission(context, perm) != PackageManager.PERMISSION_GRANTED)
|
|
||||||
granted = false;
|
|
||||||
}
|
|
||||||
if (granted) {
|
|
||||||
Const.EXTERNAL_PATH.mkdirs();
|
|
||||||
callback.run();
|
|
||||||
} else {
|
|
||||||
// Passed in context should be an activity if not granted, need to show dialog!
|
|
||||||
if (context instanceof BaseActivity) {
|
|
||||||
BaseActivity activity = (BaseActivity) context;
|
|
||||||
int code = callback.hashCode() & 0xFFFF;
|
|
||||||
activity.resultListeners.put(code, ((i, d) -> callback.run()));
|
|
||||||
ActivityCompat.requestPermissions(activity, permissions, code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
onActivityResultListener(requestCode, resultCode, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onActivityResultListener(int requestCode, int resultCode, Intent data) {
|
|
||||||
ActivityResultListener listener = resultListeners.get(requestCode);
|
|
||||||
if (listener != null) {
|
|
||||||
resultListeners.remove(requestCode);
|
|
||||||
listener.onActivityResult(resultCode, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startActivityForResult(Intent intent, int requestCode, ActivityResultListener listener) {
|
|
||||||
resultListeners.put(requestCode, listener);
|
|
||||||
super.startActivityForResult(intent, requestCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
||||||
boolean grant = true;
|
|
||||||
for (int result : grantResults) {
|
|
||||||
if (result != PackageManager.PERMISSION_GRANTED)
|
|
||||||
grant = false;
|
|
||||||
}
|
|
||||||
if (grant)
|
|
||||||
onActivityResultListener(requestCode, 0, null);
|
|
||||||
else
|
|
||||||
resultListeners.remove(requestCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ActivityResultListener {
|
|
||||||
void onActivityResult(int resultCode, Intent data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SharedPreferences getSharedPreferences(String name, int mode) {
|
|
||||||
if (TextUtils.equals(name, getPackageName() + "_preferences"))
|
|
||||||
return app.prefs;
|
|
||||||
return super.getSharedPreferences(name, mode);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.base;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.App;
|
|
||||||
import com.topjohnwu.magisk.utils.Event;
|
|
||||||
|
|
||||||
import butterknife.Unbinder;
|
|
||||||
|
|
||||||
public abstract class BaseFragment extends Fragment implements Event.AutoListener {
|
|
||||||
|
|
||||||
public App app = App.self;
|
|
||||||
protected Unbinder unbinder = null;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
Event.register(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
Event.unregister(this);
|
|
||||||
super.onPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
super.onDestroyView();
|
|
||||||
if (unbinder != null)
|
|
||||||
unbinder.unbind();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void startActivityForResult(Intent intent, int requestCode) {
|
|
||||||
startActivityForResult(intent, requestCode, (resultCode, data) ->
|
|
||||||
onActivityResult(requestCode, resultCode, data));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startActivityForResult(Intent intent, int requestCode,
|
|
||||||
BaseActivity.ActivityResultListener listener) {
|
|
||||||
((BaseActivity) requireActivity()).startActivityForResult(intent, requestCode, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void runWithExternalRW(Runnable callback) {
|
|
||||||
((BaseActivity) requireActivity()).runWithExternalRW(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int[] getListeningEvents() {
|
|
||||||
return BaseActivity.EMPTY_INT_ARRAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEvent(int event) {}
|
|
||||||
}
|
|
@ -8,6 +8,10 @@ import android.view.LayoutInflater;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.App;
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
import com.topjohnwu.magisk.utils.Event;
|
||||||
|
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceCategory;
|
import androidx.preference.PreferenceCategory;
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
@ -16,10 +20,6 @@ import androidx.preference.PreferenceScreen;
|
|||||||
import androidx.preference.PreferenceViewHolder;
|
import androidx.preference.PreferenceViewHolder;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.App;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.utils.Event;
|
|
||||||
|
|
||||||
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat
|
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener, Event.AutoListener {
|
implements SharedPreferences.OnSharedPreferenceChangeListener, Event.AutoListener {
|
||||||
|
|
||||||
@ -28,21 +28,21 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat
|
|||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
View v = super.onCreateView(inflater, container, savedInstanceState);
|
View v = super.onCreateView(inflater, container, savedInstanceState);
|
||||||
app.prefs.registerOnSharedPreferenceChangeListener(this);
|
app.getPrefs().registerOnSharedPreferenceChangeListener(this);
|
||||||
Event.register(this);
|
Event.register(this);
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
app.prefs.unregisterOnSharedPreferenceChangeListener(this);
|
app.getPrefs().unregisterOnSharedPreferenceChangeListener(this);
|
||||||
Event.unregister(this);
|
Event.unregister(this);
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int[] getListeningEvents() {
|
public int[] getListeningEvents() {
|
||||||
return BaseActivity.EMPTY_INT_ARRAY;
|
return new int[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.base
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
interface IBaseLeanback {
|
||||||
|
|
||||||
|
fun runWithExternalRW(callback: Runnable)
|
||||||
|
fun runWithPermissions(vararg permissions: String, callback: Runnable)
|
||||||
|
fun startActivityForResult(intent: Intent, requestCode: Int, listener: ActivityResultListener)
|
||||||
|
|
||||||
|
}
|
191
app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt
Normal file
191
app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.base
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentTransaction
|
||||||
|
import com.karumi.dexter.Dexter
|
||||||
|
import com.karumi.dexter.MultiplePermissionsReport
|
||||||
|
import com.karumi.dexter.PermissionToken
|
||||||
|
import com.karumi.dexter.listener.PermissionRequest
|
||||||
|
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
|
||||||
|
import com.ncapdevi.fragnav.FragNavController
|
||||||
|
import com.ncapdevi.fragnav.FragNavTransactionOptions
|
||||||
|
import com.skoumal.teanity.viewevents.ViewEvent
|
||||||
|
import com.topjohnwu.magisk.Config
|
||||||
|
import com.topjohnwu.magisk.model.events.BackPressEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.PermissionEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.ViewActionEvent
|
||||||
|
import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder
|
||||||
|
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
|
||||||
|
import com.topjohnwu.magisk.model.navigation.Navigator
|
||||||
|
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
|
||||||
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
|
||||||
|
abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
|
||||||
|
MagiskLeanbackActivity<ViewModel, Binding>(), FragNavController.RootFragmentListener,
|
||||||
|
Navigator {
|
||||||
|
|
||||||
|
override val numberOfRootFragments: Int get() = baseFragments.size
|
||||||
|
override val baseFragments: List<KClass<out Fragment>> = listOf()
|
||||||
|
|
||||||
|
protected open val defaultPosition: Int = 0
|
||||||
|
|
||||||
|
protected val navigationController get() = if (navHostId == 0) null else _navigationController
|
||||||
|
private val _navigationController by lazy {
|
||||||
|
if (navHostId == 0) throw IllegalStateException("Did you forget to override \"navHostId\"?")
|
||||||
|
FragNavController(supportFragmentManager, navHostId)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val isDarkTheme = Config.get<Boolean>(Config.Key.DARK_THEME)
|
||||||
|
val theme = if (isDarkTheme) {
|
||||||
|
AppCompatDelegate.MODE_NIGHT_YES
|
||||||
|
} else {
|
||||||
|
AppCompatDelegate.MODE_NIGHT_NO
|
||||||
|
}
|
||||||
|
AppCompatDelegate.setDefaultNightMode(theme)
|
||||||
|
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
navigationController?.apply {
|
||||||
|
rootFragmentListener = this@MagiskActivity
|
||||||
|
initialize(defaultPosition, savedInstanceState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
navigationController?.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onEventDispatched(event: ViewEvent) {
|
||||||
|
super.onEventDispatched(event)
|
||||||
|
when (event) {
|
||||||
|
is BackPressEvent -> onBackPressed()
|
||||||
|
is MagiskNavigationEvent -> navigateTo(event)
|
||||||
|
is ViewActionEvent -> event.action(this)
|
||||||
|
is PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) {
|
||||||
|
onSuccess { event.callback.onNext(true) }
|
||||||
|
onFailure {
|
||||||
|
event.callback.onNext(false)
|
||||||
|
event.callback.onError(SecurityException("User refused permissions"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRootFragment(index: Int) = baseFragments[index].java.newInstance()
|
||||||
|
|
||||||
|
override fun navigateTo(event: MagiskNavigationEvent) {
|
||||||
|
val directions = event.navDirections
|
||||||
|
|
||||||
|
navigationController?.defaultTransactionOptions = FragNavTransactionOptions.newBuilder()
|
||||||
|
.customAnimations(event.animOptions)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
navigationController?.currentStack
|
||||||
|
?.indexOfFirst { it.javaClass == event.navOptions.popUpTo }
|
||||||
|
?.let { if (it == -1) null else it } // invalidate if class is not found
|
||||||
|
?.let { if (event.navOptions.inclusive) it + 1 else it }
|
||||||
|
?.let { navigationController?.popFragments(it) }
|
||||||
|
|
||||||
|
when (directions.isActivity) {
|
||||||
|
true -> navigateToActivity(event)
|
||||||
|
else -> navigateToFragment(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToActivity(event: MagiskNavigationEvent) {
|
||||||
|
val destination = event.navDirections.destination?.java ?: let {
|
||||||
|
Timber.e("Cannot navigate to null destination")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val options = event.navOptions
|
||||||
|
|
||||||
|
Intent(this, destination)
|
||||||
|
.putExtras(event.navDirections.args)
|
||||||
|
.apply {
|
||||||
|
if (options.singleTop) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
if (options.clearTask) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
}
|
||||||
|
.let { startActivity(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToFragment(event: MagiskNavigationEvent) {
|
||||||
|
val destination = event.navDirections.destination?.java ?: let {
|
||||||
|
Timber.e("Cannot navigate to null destination")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val index = baseFragments.indexOfFirst { it.java.name == destination.name }) {
|
||||||
|
-1 -> destination.newInstance()
|
||||||
|
.apply { arguments = event.navDirections.args }
|
||||||
|
.let { navigationController?.pushFragment(it) }
|
||||||
|
// When it's desired that fragments of same class are put on top of one another edit this
|
||||||
|
else -> navigationController?.switchTab(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
val fragment = navigationController?.currentFrag as? MagiskFragment<*, *>
|
||||||
|
|
||||||
|
if (fragment?.onBackPressed() == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
navigationController?.popFragment() ?: throw UnsupportedOperationException()
|
||||||
|
} catch (e: UnsupportedOperationException) {
|
||||||
|
when {
|
||||||
|
navigationController?.currentStackIndex != defaultPosition -> {
|
||||||
|
val options = FragNavTransactionOptions.newBuilder()
|
||||||
|
.transition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
|
||||||
|
.build()
|
||||||
|
navigationController?.switchTab(defaultPosition, options)
|
||||||
|
}
|
||||||
|
else -> super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openUrl(url: String) = Utils.openLink(this, url.toUri())
|
||||||
|
|
||||||
|
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
|
||||||
|
val request = PermissionRequestBuilder().apply(builder).build()
|
||||||
|
Dexter.withActivity(this)
|
||||||
|
.withPermissions(*permissions)
|
||||||
|
.withListener(object : MultiplePermissionsListener {
|
||||||
|
override fun onPermissionsChecked(report: MultiplePermissionsReport?) =
|
||||||
|
if (report?.areAllPermissionsGranted() == true) {
|
||||||
|
request.onSuccess()
|
||||||
|
} else {
|
||||||
|
request.onFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPermissionRationaleShouldBeShown(
|
||||||
|
permissions: MutableList<PermissionRequest>?,
|
||||||
|
token: PermissionToken?
|
||||||
|
) = request.onShowRationale(permissions.orEmpty().map { it.name })
|
||||||
|
})
|
||||||
|
.check()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) =
|
||||||
|
customAnimations(options.enter, options.exit, options.popEnter, options.popExit).apply {
|
||||||
|
if (!options.anySet) {
|
||||||
|
transition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.base
|
||||||
|
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.skoumal.teanity.view.TeanityFragment
|
||||||
|
import com.skoumal.teanity.viewevents.ViewEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.BackPressEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.PermissionEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.ViewActionEvent
|
||||||
|
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
|
||||||
|
import com.topjohnwu.magisk.model.navigation.Navigator
|
||||||
|
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
|
||||||
|
abstract class MagiskFragment<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
|
||||||
|
TeanityFragment<ViewModel, Binding>(), Navigator {
|
||||||
|
|
||||||
|
protected val magiskActivity get() = activity as MagiskActivity<*, *>
|
||||||
|
|
||||||
|
// We don't need nested fragments
|
||||||
|
override val baseFragments: List<KClass<Fragment>> = listOf()
|
||||||
|
|
||||||
|
override fun navigateTo(event: MagiskNavigationEvent) = magiskActivity.navigateTo(event)
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onEventDispatched(event: ViewEvent) {
|
||||||
|
super.onEventDispatched(event)
|
||||||
|
when (event) {
|
||||||
|
is BackPressEvent -> magiskActivity.onBackPressed()
|
||||||
|
is MagiskNavigationEvent -> navigateTo(event)
|
||||||
|
is ViewActionEvent -> event.action(requireActivity())
|
||||||
|
is PermissionEvent -> magiskActivity.withPermissions(*event.permissions.toTypedArray()) {
|
||||||
|
onSuccess { event.callback.onNext(true) }
|
||||||
|
onFailure {
|
||||||
|
event.callback.onNext(false)
|
||||||
|
event.callback.onError(SecurityException("User refused permissions"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
|
||||||
|
magiskActivity.withPermissions(*permissions, builder = builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openLink(url: String) = magiskActivity.openUrl(url)
|
||||||
|
|
||||||
|
open fun onBackPressed(): Boolean = false
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.base
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.collection.SparseArrayCompat
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import com.karumi.dexter.Dexter
|
||||||
|
import com.karumi.dexter.MultiplePermissionsReport
|
||||||
|
import com.karumi.dexter.PermissionToken
|
||||||
|
import com.karumi.dexter.listener.PermissionRequest
|
||||||
|
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
|
||||||
|
import com.skoumal.teanity.view.TeanityActivity
|
||||||
|
import com.topjohnwu.magisk.Const
|
||||||
|
|
||||||
|
abstract class MagiskLeanbackActivity<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
|
||||||
|
TeanityActivity<ViewModel, Binding>(), IBaseLeanback {
|
||||||
|
|
||||||
|
private val resultListeners = SparseArrayCompat<ActivityResultListener>()
|
||||||
|
|
||||||
|
@Deprecated("Permissions will be checked in a different streamlined way")
|
||||||
|
fun runWithExternalRW(callback: () -> Unit) = runWithExternalRW(Runnable { callback() })
|
||||||
|
|
||||||
|
@Deprecated("Permissions will be checked in a different streamlined way")
|
||||||
|
override fun runWithExternalRW(callback: Runnable) {
|
||||||
|
runWithPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, callback = callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Permissions will be checked in a different streamlined way")
|
||||||
|
override fun runWithPermissions(vararg permissions: String, callback: Runnable) {
|
||||||
|
Dexter.withActivity(this)
|
||||||
|
.withPermissions(*permissions)
|
||||||
|
.withListener(object : MultiplePermissionsListener {
|
||||||
|
override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
|
||||||
|
if (report?.areAllPermissionsGranted() == true) {
|
||||||
|
Const.EXTERNAL_PATH.mkdirs()
|
||||||
|
callback.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPermissionRationaleShouldBeShown(
|
||||||
|
permissions: MutableList<PermissionRequest>?,
|
||||||
|
token: PermissionToken?
|
||||||
|
) = Unit
|
||||||
|
})
|
||||||
|
.check()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
resultListeners.get(requestCode)?.apply {
|
||||||
|
resultListeners.remove(requestCode)
|
||||||
|
onActivityResult(resultCode, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startActivityForResult(
|
||||||
|
intent: Intent,
|
||||||
|
requestCode: Int,
|
||||||
|
listener: ActivityResultListener
|
||||||
|
) {
|
||||||
|
resultListeners.put(requestCode, listener)
|
||||||
|
startActivityForResult(intent, requestCode)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.base
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import com.skoumal.teanity.viewmodel.LoadingViewModel
|
||||||
|
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.Event
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.subjects.PublishSubject
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
|
abstract class MagiskViewModel : LoadingViewModel(), Event.AutoListener {
|
||||||
|
|
||||||
|
override fun onEvent(event: Int) = Timber.i("Event of $event was not handled")
|
||||||
|
override fun getListeningEvents(): IntArray = intArrayOf()
|
||||||
|
|
||||||
|
fun withView(action: Activity.() -> Unit) {
|
||||||
|
ViewActionEvent(action).publish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withPermissions(vararg permissions: String): Observable<Boolean> {
|
||||||
|
val subject = PublishSubject.create<Boolean>()
|
||||||
|
return subject.doOnSubscribe { PermissionEvent(permissions.toList(), subject).publish() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun back() = BackPressEvent().publish()
|
||||||
|
|
||||||
|
}
|
@ -1,274 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.flash;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.model.adapters.StringListAdapter;
|
|
||||||
import com.topjohnwu.magisk.tasks.FlashZip;
|
|
||||||
import com.topjohnwu.magisk.tasks.MagiskInstaller;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.utils.RootUtils;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.superuser.CallbackList;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileWriter;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import butterknife.BindColor;
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.OnClick;
|
|
||||||
|
|
||||||
public class FlashActivity extends BaseActivity {
|
|
||||||
|
|
||||||
@BindView(R.id.toolbar) Toolbar toolbar;
|
|
||||||
@BindView(R.id.button_panel) LinearLayout buttonPanel;
|
|
||||||
@BindView(R.id.reboot) Button reboot;
|
|
||||||
@BindView(R.id.recyclerView) RecyclerView rv;
|
|
||||||
@BindColor(android.R.color.white) int white;
|
|
||||||
|
|
||||||
private List<String> console, logs;
|
|
||||||
|
|
||||||
@OnClick(R.id.reboot)
|
|
||||||
void reboot() {
|
|
||||||
RootUtils.reboot();
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.save_logs)
|
|
||||||
void saveLogs() {
|
|
||||||
runWithExternalRW(() -> {
|
|
||||||
Calendar now = Calendar.getInstance();
|
|
||||||
String filename = String.format(Locale.US,
|
|
||||||
"magisk_install_log_%04d%02d%02d_%02d%02d%02d.log",
|
|
||||||
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
|
|
||||||
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
|
|
||||||
now.get(Calendar.MINUTE), now.get(Calendar.SECOND));
|
|
||||||
|
|
||||||
File logFile = new File(Const.EXTERNAL_PATH, filename);
|
|
||||||
try (FileWriter writer = new FileWriter(logFile)) {
|
|
||||||
for (String s : logs) {
|
|
||||||
writer.write(s);
|
|
||||||
writer.write('\n');
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Utils.toast(logFile.getPath(), Toast.LENGTH_LONG);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.close)
|
|
||||||
public void close() {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
// Prevent user accidentally press back button
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getDarkTheme() {
|
|
||||||
return R.style.AppTheme_NoDrawer_Dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_flash);
|
|
||||||
new FlashActivity_ViewBinding(this);
|
|
||||||
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
ActionBar ab = getSupportActionBar();
|
|
||||||
if (ab != null) {
|
|
||||||
ab.setTitle(R.string.flashing);
|
|
||||||
}
|
|
||||||
setFloating();
|
|
||||||
setFinishOnTouchOutside(false);
|
|
||||||
if (!Shell.rootAccess())
|
|
||||||
reboot.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
logs = Collections.synchronizedList(new ArrayList<>());
|
|
||||||
console = new ConsoleList();
|
|
||||||
rv.setAdapter(new ConsoleAdapter());
|
|
||||||
|
|
||||||
Intent intent = getIntent();
|
|
||||||
Uri uri = intent.getData();
|
|
||||||
|
|
||||||
switch (intent.getStringExtra(Const.Key.FLASH_ACTION)) {
|
|
||||||
case Const.Value.FLASH_ZIP:
|
|
||||||
new FlashModule(uri).exec();
|
|
||||||
break;
|
|
||||||
case Const.Value.UNINSTALL:
|
|
||||||
new Uninstall(uri).exec();
|
|
||||||
break;
|
|
||||||
case Const.Value.FLASH_MAGISK:
|
|
||||||
new DirectInstall().exec();
|
|
||||||
break;
|
|
||||||
case Const.Value.FLASH_INACTIVE_SLOT:
|
|
||||||
new SecondSlot().exec();
|
|
||||||
break;
|
|
||||||
case Const.Value.PATCH_FILE:
|
|
||||||
new PatchFile(uri).exec();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ConsoleAdapter extends StringListAdapter<ConsoleAdapter.ViewHolder> {
|
|
||||||
|
|
||||||
ConsoleAdapter() {
|
|
||||||
super(console, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int itemLayoutRes() {
|
|
||||||
return R.layout.list_item_console;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ViewHolder createViewHolder(@NonNull View v) {
|
|
||||||
return new ViewHolder(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewHolder extends StringListAdapter.ViewHolder {
|
|
||||||
|
|
||||||
public ViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
txt.setTextColor(white);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int textViewResId() {
|
|
||||||
return R.id.txt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ConsoleList extends CallbackList<String> {
|
|
||||||
|
|
||||||
ConsoleList() {
|
|
||||||
super(new ArrayList<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUI() {
|
|
||||||
rv.getAdapter().notifyItemChanged(size() - 1);
|
|
||||||
rv.postDelayed(() -> rv.smoothScrollToPosition(size() - 1), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAddElement(String s) {
|
|
||||||
logs.add(s);
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String set(int i, String s) {
|
|
||||||
String ret = super.set(i, s);
|
|
||||||
UiThreadHandler.run(this::updateUI);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FlashModule extends FlashZip {
|
|
||||||
|
|
||||||
FlashModule(Uri uri) {
|
|
||||||
super(uri, console, logs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResult(boolean success) {
|
|
||||||
if (success) {
|
|
||||||
Utils.loadModules();
|
|
||||||
} else {
|
|
||||||
console.add("! Installation failed");
|
|
||||||
reboot.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
buttonPanel.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Uninstall extends FlashModule {
|
|
||||||
|
|
||||||
Uninstall(Uri uri) {
|
|
||||||
super(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResult(boolean success) {
|
|
||||||
if (success)
|
|
||||||
UiThreadHandler.handler.postDelayed(Shell.su("pm uninstall " + getPackageName())::exec, 3000);
|
|
||||||
else
|
|
||||||
super.onResult(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private abstract class BaseInstaller extends MagiskInstaller {
|
|
||||||
BaseInstaller() {
|
|
||||||
super(console, logs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResult(boolean success) {
|
|
||||||
if (success) {
|
|
||||||
console.add("- All done!");
|
|
||||||
} else {
|
|
||||||
Shell.sh("rm -rf " + installDir).submit();
|
|
||||||
console.add("! Installation failed");
|
|
||||||
reboot.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
buttonPanel.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DirectInstall extends BaseInstaller {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean operations() {
|
|
||||||
return findImage() && extractZip() && patchBoot() && flashBoot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SecondSlot extends BaseInstaller {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean operations() {
|
|
||||||
return findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PatchFile extends BaseInstaller {
|
|
||||||
|
|
||||||
private Uri uri;
|
|
||||||
|
|
||||||
PatchFile(Uri u) {
|
|
||||||
uri = u;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean operations() {
|
|
||||||
return extractZip() && handleFile(uri) && patchBoot() && storeBoot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,24 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.flash
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.Const
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.databinding.ActivityFlashBinding
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskActivity
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
|
open class FlashActivity : MagiskActivity<FlashViewModel, ActivityFlashBinding>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.activity_flash
|
||||||
|
override val viewModel: FlashViewModel by viewModel {
|
||||||
|
val uri = intent.data
|
||||||
|
val action = intent.getStringExtra(Const.Key.FLASH_ACTION) ?: let { finish();"" }
|
||||||
|
parametersOf(action, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (viewModel.loading) return
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.flash
|
||||||
|
|
||||||
|
import android.Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Handler
|
||||||
|
import androidx.core.os.postDelayed
|
||||||
|
import androidx.databinding.ObservableArrayList
|
||||||
|
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||||
|
import com.skoumal.teanity.extensions.subscribeK
|
||||||
|
import com.skoumal.teanity.util.DiffObservableList
|
||||||
|
import com.skoumal.teanity.util.KObservableField
|
||||||
|
import com.skoumal.teanity.viewevents.SnackbarEvent
|
||||||
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.Const
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
|
||||||
|
import com.topjohnwu.magisk.model.flash.FlashResultListener
|
||||||
|
import com.topjohnwu.magisk.model.flash.Flashing
|
||||||
|
import com.topjohnwu.magisk.model.flash.Patching
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||||
|
import com.topjohnwu.magisk.utils.*
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class FlashViewModel(
|
||||||
|
action: String,
|
||||||
|
uri: Uri?,
|
||||||
|
private val resources: Resources
|
||||||
|
) : MagiskViewModel(), 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 rawItems = ObservableArrayList<String>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
rawItems.sendUpdatesTo(items) { it.map { ConsoleRvItem(it) } }
|
||||||
|
|
||||||
|
state = State.LOADING
|
||||||
|
|
||||||
|
val uri = uri ?: Uri.EMPTY
|
||||||
|
when (action) {
|
||||||
|
Const.Value.FLASH_ZIP -> Flashing
|
||||||
|
.Install(uri, rawItems, rawItems, this)
|
||||||
|
.exec()
|
||||||
|
Const.Value.UNINSTALL -> Flashing
|
||||||
|
.Uninstall(uri, rawItems, rawItems, this)
|
||||||
|
.exec()
|
||||||
|
Const.Value.FLASH_MAGISK -> Patching
|
||||||
|
.Direct(rawItems, rawItems, this)
|
||||||
|
.exec()
|
||||||
|
Const.Value.FLASH_INACTIVE_SLOT -> Patching
|
||||||
|
.SecondSlot(rawItems, rawItems, this)
|
||||||
|
.exec()
|
||||||
|
Const.Value.PATCH_FILE -> Patching
|
||||||
|
.File(uri, rawItems, rawItems, 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(timeFormatFull) }
|
||||||
|
.map { Const.MAGISK_INSTALL_LOG_FILENAME.format(it) }
|
||||||
|
.map { File(Const.EXTERNAL_PATH, it) }
|
||||||
|
.map { file ->
|
||||||
|
val log = items.filterIsInstance<ConsoleRvItem>()
|
||||||
|
.joinToString("\n") { it.item }
|
||||||
|
file.writeText(log)
|
||||||
|
file.path
|
||||||
|
}
|
||||||
|
.subscribeK { SnackbarEvent(it).publish() }
|
||||||
|
.add()
|
||||||
|
|
||||||
|
fun restartPressed() = RootUtils.reboot()
|
||||||
|
|
||||||
|
fun backPressed() = back()
|
||||||
|
|
||||||
|
}
|
123
app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt
Normal file
123
app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.hide
|
||||||
|
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||||
|
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||||
|
import com.skoumal.teanity.extensions.subscribeK
|
||||||
|
import com.skoumal.teanity.rxbus.RxBus
|
||||||
|
import com.skoumal.teanity.util.DiffObservableList
|
||||||
|
import com.skoumal.teanity.util.KObservableField
|
||||||
|
import com.topjohnwu.magisk.App
|
||||||
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
||||||
|
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.HideRvItem
|
||||||
|
import com.topjohnwu.magisk.model.events.HideProcessEvent
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||||
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
|
import com.topjohnwu.magisk.utils.toSingle
|
||||||
|
import com.topjohnwu.magisk.utils.update
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import io.reactivex.Single
|
||||||
|
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class HideViewModel(
|
||||||
|
private val packageManager: PackageManager,
|
||||||
|
rxBus: RxBus
|
||||||
|
) : MagiskViewModel() {
|
||||||
|
|
||||||
|
val query = KObservableField("")
|
||||||
|
val isShowSystem = KObservableField(false)
|
||||||
|
|
||||||
|
private val allItems = mutableListOf<ComparableRvItem<*>>()
|
||||||
|
val items = DiffObservableList(ComparableRvItem.callback)
|
||||||
|
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
|
||||||
|
item.bind(itemBinding)
|
||||||
|
itemBinding.bindExtra(BR.viewModel, this@HideViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
rxBus.register<HideProcessEvent>()
|
||||||
|
.subscribeK { toggleItem(it.item) }
|
||||||
|
.add()
|
||||||
|
|
||||||
|
isShowSystem.addOnPropertyChangedCallback { query() }
|
||||||
|
query.addOnPropertyChangedCallback { query() }
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
// fetching this for every item is nonsensical, so we add .cache() so the response is all
|
||||||
|
// the same for every single mapped item, it only actually executes the whole thing the
|
||||||
|
// first time around.
|
||||||
|
val hideTargets = Shell.su("magiskhide --ls").toSingle()
|
||||||
|
.map { it.exec().out }
|
||||||
|
.flattenAsFlowable { it }
|
||||||
|
.map { HideTarget(it) }
|
||||||
|
.toList()
|
||||||
|
.cache()
|
||||||
|
|
||||||
|
Single.fromCallable { packageManager.getInstalledApplications(0) }
|
||||||
|
.flattenAsFlowable { it }
|
||||||
|
.filter { it.enabled && !blacklist.contains(it.packageName) }
|
||||||
|
.map {
|
||||||
|
val label = Utils.getAppLabel(it, packageManager)
|
||||||
|
val icon = it.loadIcon(packageManager)
|
||||||
|
HideAppInfo(it, label, icon)
|
||||||
|
}
|
||||||
|
.filter { it.processes.isNotEmpty() }
|
||||||
|
.map { HideRvItem(it, hideTargets.blockingGet()) }
|
||||||
|
.toList()
|
||||||
|
.map { it.sortBy { it.item.info.name }; it }
|
||||||
|
.doOnSuccess { allItems.update(it) }
|
||||||
|
.flatMap { queryRaw() }
|
||||||
|
.applyViewModel(this)
|
||||||
|
.subscribeK(onError = Timber::e) { items.update(it.first, it.second) }
|
||||||
|
.add()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun query() = queryRaw()
|
||||||
|
.subscribeK { items.update(it.first, it.second) }
|
||||||
|
.add()
|
||||||
|
|
||||||
|
private fun queryRaw(
|
||||||
|
showSystem: Boolean = isShowSystem.value,
|
||||||
|
query: String = this.query.value
|
||||||
|
) = allItems.toSingle()
|
||||||
|
.map { it.filterIsInstance<HideRvItem>() }
|
||||||
|
.flattenAsFlowable { it }
|
||||||
|
.filter {
|
||||||
|
it.item.name.contains(query, ignoreCase = true) ||
|
||||||
|
it.item.processes.any { it.contains(query, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
.filter { if (showSystem) true else it.item.info.flags and ApplicationInfo.FLAG_SYSTEM == 0 }
|
||||||
|
.toList()
|
||||||
|
.map { it to items.calculateDiff(it) }
|
||||||
|
|
||||||
|
private fun toggleItem(item: HideProcessRvItem) {
|
||||||
|
val state = if (item.isHidden.value) "add" else "rm"
|
||||||
|
"magiskhide --%s %s %s".format(state, item.packageName, item.process)
|
||||||
|
.let { Shell.su(it) }
|
||||||
|
.toSingle()
|
||||||
|
.map { it.submit() }
|
||||||
|
.subscribeK()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val blacklist = listOf(
|
||||||
|
App.self.packageName,
|
||||||
|
"android",
|
||||||
|
"com.android.chrome",
|
||||||
|
"com.chrome.beta",
|
||||||
|
"com.chrome.dev",
|
||||||
|
"com.chrome.canary",
|
||||||
|
"com.android.webview",
|
||||||
|
"com.google.android.webview"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,101 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.hide;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.SearchView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Config;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.model.adapters.ApplicationAdapter;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseFragment;
|
|
||||||
import com.topjohnwu.magisk.utils.Event;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class MagiskHideFragment extends BaseFragment {
|
|
||||||
|
|
||||||
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
|
|
||||||
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
|
||||||
|
|
||||||
private SearchView search;
|
|
||||||
private ApplicationAdapter adapter;
|
|
||||||
private SearchView.OnQueryTextListener searchListener;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
|
||||||
View view = inflater.inflate(R.layout.fragment_magisk_hide, container, false);
|
|
||||||
unbinder = new MagiskHideFragment_ViewBinding(this, view);
|
|
||||||
|
|
||||||
adapter = new ApplicationAdapter(requireActivity());
|
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
|
|
||||||
mSwipeRefreshLayout.setRefreshing(true);
|
|
||||||
mSwipeRefreshLayout.setOnRefreshListener(adapter::refresh);
|
|
||||||
|
|
||||||
searchListener = new SearchView.OnQueryTextListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onQueryTextSubmit(String query) {
|
|
||||||
adapter.filter(query);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onQueryTextChange(String newText) {
|
|
||||||
adapter.filter(newText);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
requireActivity().setTitle(R.string.magiskhide);
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
|
||||||
inflater.inflate(R.menu.menu_magiskhide, menu);
|
|
||||||
search = (SearchView) menu.findItem(R.id.app_search).getActionView();
|
|
||||||
search.setOnQueryTextListener(searchListener);
|
|
||||||
menu.findItem(R.id.show_system).setChecked(Config.get(Config.Key.SHOW_SYSTEM_APP));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
if (item.getItemId() == R.id.show_system) {
|
|
||||||
boolean showSystem = !item.isChecked();
|
|
||||||
item.setChecked(showSystem);
|
|
||||||
Config.set(Config.Key.SHOW_SYSTEM_APP, showSystem);
|
|
||||||
adapter.setShowSystem(showSystem);
|
|
||||||
adapter.filter(search.getQuery().toString());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int[] getListeningEvents() {
|
|
||||||
return new int[] {Event.MAGISK_HIDE_DONE};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEvent(int event) {
|
|
||||||
mSwipeRefreshLayout.setRefreshing(false);
|
|
||||||
adapter.filter(search.getQuery().toString());
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,64 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.hide
|
||||||
|
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.widget.SearchView
|
||||||
|
import com.topjohnwu.magisk.Config
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.databinding.FragmentMagiskHideBinding
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskFragment
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
|
class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBinding>(),
|
||||||
|
SearchView.OnQueryTextListener {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.fragment_magisk_hide
|
||||||
|
override val viewModel: HideViewModel by viewModel()
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
requireActivity().setTitle(R.string.magiskhide)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.menu_magiskhide, menu)
|
||||||
|
menu.apply {
|
||||||
|
(findItem(R.id.app_search).actionView as? SearchView)
|
||||||
|
?.setOnQueryTextListener(this@MagiskHideFragment)
|
||||||
|
|
||||||
|
val showSystem = Config.get<Boolean>(Config.Key.SHOW_SYSTEM_APP)
|
||||||
|
|
||||||
|
findItem(R.id.show_system).isChecked = showSystem
|
||||||
|
viewModel.isShowSystem.value = showSystem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
if (item.itemId == R.id.show_system) {
|
||||||
|
val showSystem = !item.isChecked
|
||||||
|
item.isChecked = showSystem
|
||||||
|
Config.set(Config.Key.SHOW_SYSTEM_APP, showSystem)
|
||||||
|
viewModel.isShowSystem.value = showSystem
|
||||||
|
//adapter!!.setShowSystem(showSystem)
|
||||||
|
//adapter!!.filter(search!!.query.toString())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
viewModel.query.value = query.orEmpty()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(query: String?): Boolean {
|
||||||
|
viewModel.query.value = query.orEmpty()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/*override fun onEvent(event: Int) {
|
||||||
|
//mSwipeRefreshLayout!!.isRefreshing = false
|
||||||
|
adapter!!.filter(search!!.query.toString())
|
||||||
|
}*/
|
||||||
|
}
|
232
app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt
Normal file
232
app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.home
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||||
|
import com.skoumal.teanity.util.KObservableField
|
||||||
|
import com.topjohnwu.magisk.*
|
||||||
|
import com.topjohnwu.magisk.model.events.*
|
||||||
|
import com.topjohnwu.magisk.model.observer.Observer
|
||||||
|
import com.topjohnwu.magisk.tasks.CheckUpdates
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||||
|
import com.topjohnwu.magisk.utils.Event
|
||||||
|
import com.topjohnwu.magisk.utils.ISafetyNetHelper
|
||||||
|
import com.topjohnwu.magisk.utils.toggle
|
||||||
|
import com.topjohnwu.net.Networking
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
||||||
|
|
||||||
|
class HomeViewModel(
|
||||||
|
private val resources: Resources,
|
||||||
|
private val app: App
|
||||||
|
) : MagiskViewModel() {
|
||||||
|
|
||||||
|
val isAdvancedExpanded = KObservableField(false)
|
||||||
|
|
||||||
|
val isForceEncryption = KObservableField(Config.keepEnc)
|
||||||
|
val isKeepVerity = KObservableField(Config.keepVerity)
|
||||||
|
|
||||||
|
val magiskState = KObservableField(MagiskState.LOADING)
|
||||||
|
val magiskStateText = Observer(magiskState) {
|
||||||
|
when (magiskState.value) {
|
||||||
|
MagiskState.NO_ROOT -> TODO()
|
||||||
|
MagiskState.NOT_INSTALLED -> resources.getString(R.string.magisk_version_error)
|
||||||
|
MagiskState.UP_TO_DATE -> resources.getString(R.string.magisk_up_to_date)
|
||||||
|
MagiskState.LOADING -> resources.getString(R.string.checking_for_updates)
|
||||||
|
MagiskState.OBSOLETE -> resources.getString(R.string.magisk_update_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val magiskCurrentVersion = KObservableField("")
|
||||||
|
val magiskLatestVersion = KObservableField("")
|
||||||
|
val magiskAdditionalInfo = Observer(magiskState) {
|
||||||
|
if (Config.get<Boolean>(Config.Key.COREONLY))
|
||||||
|
resources.getString(R.string.core_only_enabled)
|
||||||
|
else
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
val managerState = KObservableField(MagiskState.LOADING)
|
||||||
|
val managerStateText = Observer(managerState) {
|
||||||
|
when (managerState.value) {
|
||||||
|
MagiskState.NO_ROOT -> "wtf"
|
||||||
|
MagiskState.NOT_INSTALLED -> resources.getString(R.string.invalid_update_channel)
|
||||||
|
MagiskState.UP_TO_DATE -> resources.getString(R.string.manager_up_to_date)
|
||||||
|
MagiskState.LOADING -> resources.getString(R.string.checking_for_updates)
|
||||||
|
MagiskState.OBSOLETE -> resources.getString(R.string.manager_update_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val managerCurrentVersion = KObservableField("")
|
||||||
|
val managerLatestVersion = KObservableField("")
|
||||||
|
val managerAdditionalInfo = Observer(managerState) {
|
||||||
|
if (app.packageName != BuildConfig.APPLICATION_ID)
|
||||||
|
"(${app.packageName})"
|
||||||
|
else
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
val safetyNetTitle = KObservableField(resources.getString(R.string.safetyNet_check_text))
|
||||||
|
val ctsState = KObservableField(SafetyNetState.IDLE)
|
||||||
|
val basicIntegrityState = KObservableField(SafetyNetState.IDLE)
|
||||||
|
val safetyNetState = Observer(ctsState, basicIntegrityState) {
|
||||||
|
val cts = ctsState.value
|
||||||
|
val basic = basicIntegrityState.value
|
||||||
|
val states = listOf(cts, basic)
|
||||||
|
|
||||||
|
when {
|
||||||
|
states.any { it == SafetyNetState.LOADING } -> State.LOADING
|
||||||
|
states.any { it == SafetyNetState.IDLE } -> State.LOADING
|
||||||
|
else -> State.LOADED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasRoot = KObservableField(false)
|
||||||
|
|
||||||
|
private var shownDialog = false
|
||||||
|
private val current = resources.getString(R.string.current_installed)
|
||||||
|
private val latest = resources.getString(R.string.latest_version)
|
||||||
|
|
||||||
|
init {
|
||||||
|
Event.register(this)
|
||||||
|
|
||||||
|
isForceEncryption.addOnPropertyChangedCallback {
|
||||||
|
Config.keepEnc = it ?: return@addOnPropertyChangedCallback
|
||||||
|
}
|
||||||
|
isKeepVerity.addOnPropertyChangedCallback {
|
||||||
|
Config.keepVerity = it ?: return@addOnPropertyChangedCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEvent(event: Int) {
|
||||||
|
updateSelf()
|
||||||
|
ensureEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getListeningEvents(): IntArray = intArrayOf(Event.UPDATE_CHECK_DONE)
|
||||||
|
|
||||||
|
fun paypalPressed() = OpenLinkEvent(Const.Url.PAYPAL_URL).publish()
|
||||||
|
fun patreonPressed() = OpenLinkEvent(Const.Url.PATREON_URL).publish()
|
||||||
|
fun twitterPressed() = OpenLinkEvent(Const.Url.TWITTER_URL).publish()
|
||||||
|
fun githubPressed() = OpenLinkEvent(Const.Url.SOURCE_CODE_URL).publish()
|
||||||
|
fun xdaPressed() = OpenLinkEvent(Const.Url.XDA_THREAD).publish()
|
||||||
|
fun uninstallPressed() = UninstallEvent().publish()
|
||||||
|
|
||||||
|
fun advancedPressed() = isAdvancedExpanded.toggle()
|
||||||
|
|
||||||
|
fun installPressed(item: MagiskItem) = when (item) {
|
||||||
|
MagiskItem.MANAGER -> ManagerInstallEvent().publish()
|
||||||
|
MagiskItem.MAGISK -> MagiskInstallEvent().publish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cardPressed(item: MagiskItem) = when (item) {
|
||||||
|
MagiskItem.MANAGER -> ManagerChangelogEvent().publish()
|
||||||
|
MagiskItem.MAGISK -> MagiskChangelogEvent().publish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun safetyNetPressed() {
|
||||||
|
ctsState.value = SafetyNetState.LOADING
|
||||||
|
basicIntegrityState.value = SafetyNetState.LOADING
|
||||||
|
safetyNetTitle.value = resources.getString(R.string.checking_safetyNet_status)
|
||||||
|
|
||||||
|
UpdateSafetyNetEvent().publish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finishSafetyNetCheck(response: Int) = when {
|
||||||
|
response and 0x0F == 0 -> {
|
||||||
|
val hasCtsPassed = response and ISafetyNetHelper.CTS_PASS != 0
|
||||||
|
val hasBasicIntegrityPassed = response and ISafetyNetHelper.BASIC_PASS != 0
|
||||||
|
safetyNetTitle.value = resources.getString(R.string.safetyNet_check_success)
|
||||||
|
ctsState.value = if (hasCtsPassed) {
|
||||||
|
SafetyNetState.PASS
|
||||||
|
} else {
|
||||||
|
SafetyNetState.FAILED
|
||||||
|
}
|
||||||
|
basicIntegrityState.value = if (hasBasicIntegrityPassed) {
|
||||||
|
SafetyNetState.PASS
|
||||||
|
} else {
|
||||||
|
SafetyNetState.FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response == -2 -> {
|
||||||
|
ctsState.value = SafetyNetState.IDLE
|
||||||
|
basicIntegrityState.value = SafetyNetState.IDLE
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
ctsState.value = SafetyNetState.IDLE
|
||||||
|
basicIntegrityState.value = SafetyNetState.IDLE
|
||||||
|
val errorString = when (response) {
|
||||||
|
ISafetyNetHelper.RESPONSE_ERR -> R.string.safetyNet_res_invalid
|
||||||
|
else -> R.string.safetyNet_api_error
|
||||||
|
}
|
||||||
|
safetyNetTitle.value = resources.getString(errorString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
state = State.LOADING
|
||||||
|
magiskState.value = MagiskState.LOADING
|
||||||
|
managerState.value = MagiskState.LOADING
|
||||||
|
Event.reset(this)
|
||||||
|
Config.remoteMagiskVersionString = null
|
||||||
|
Config.remoteMagiskVersionCode = -1
|
||||||
|
|
||||||
|
hasRoot.value = Shell.rootAccess()
|
||||||
|
|
||||||
|
if (Networking.checkNetworkStatus(app)) {
|
||||||
|
CheckUpdates.check()
|
||||||
|
} else {
|
||||||
|
state = State.LOADING_FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSelf() {
|
||||||
|
state = State.LOADED
|
||||||
|
magiskState.value = when (Config.magiskVersionCode) {
|
||||||
|
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED
|
||||||
|
!in Config.remoteMagiskVersionCode..Int.MAX_VALUE -> MagiskState.OBSOLETE
|
||||||
|
else -> MagiskState.UP_TO_DATE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (magiskState.value != MagiskState.NOT_INSTALLED) {
|
||||||
|
magiskCurrentVersion.value = version
|
||||||
|
.format(Config.magiskVersionString, Config.magiskVersionCode)
|
||||||
|
.let { current.format(it) }
|
||||||
|
} else {
|
||||||
|
magiskCurrentVersion.value = ""
|
||||||
|
}
|
||||||
|
magiskLatestVersion.value = version
|
||||||
|
.format(Config.remoteMagiskVersionString, Config.remoteMagiskVersionCode)
|
||||||
|
.let { latest.format(it) }
|
||||||
|
|
||||||
|
managerState.value = when (Config.remoteManagerVersionCode) {
|
||||||
|
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED //wrong update channel
|
||||||
|
in (BuildConfig.VERSION_CODE + 1)..Int.MAX_VALUE -> MagiskState.OBSOLETE
|
||||||
|
else -> MagiskState.UP_TO_DATE
|
||||||
|
}
|
||||||
|
|
||||||
|
managerCurrentVersion.value = version
|
||||||
|
.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
|
||||||
|
.let { current.format(it) }
|
||||||
|
managerLatestVersion.value = version
|
||||||
|
.format(Config.remoteManagerVersionString, Config.remoteManagerVersionCode)
|
||||||
|
.let { latest.format(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureEnv() {
|
||||||
|
val invalidStates =
|
||||||
|
listOf(MagiskState.NOT_INSTALLED, MagiskState.NO_ROOT, MagiskState.LOADING)
|
||||||
|
|
||||||
|
// Don't bother checking env when magisk is not installed, loading or already has been shown
|
||||||
|
if (invalidStates.any { it == magiskState.value } || shownDialog) return
|
||||||
|
|
||||||
|
if (!Shell.su("env_check").exec().isSuccess) {
|
||||||
|
shownDialog = true
|
||||||
|
EnvFixEvent().publish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val version = "%s (%d)"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,333 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.home;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.cardview.widget.CardView;
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
||||||
import androidx.transition.ChangeBounds;
|
|
||||||
import androidx.transition.Fade;
|
|
||||||
import androidx.transition.Transition;
|
|
||||||
import androidx.transition.TransitionManager;
|
|
||||||
import androidx.transition.TransitionSet;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.BuildConfig;
|
|
||||||
import com.topjohnwu.magisk.Config;
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.tasks.CheckUpdates;
|
|
||||||
import com.topjohnwu.magisk.ui.MainActivity;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseFragment;
|
|
||||||
import com.topjohnwu.magisk.utils.Event;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.magisk.view.ArrowExpandable;
|
|
||||||
import com.topjohnwu.magisk.view.Expandable;
|
|
||||||
import com.topjohnwu.magisk.view.ExpandableViewHolder;
|
|
||||||
import com.topjohnwu.magisk.view.MarkDownWindow;
|
|
||||||
import com.topjohnwu.magisk.view.SafetyNet;
|
|
||||||
import com.topjohnwu.magisk.view.UpdateCardHolder;
|
|
||||||
import com.topjohnwu.magisk.view.dialogs.EnvFixDialog;
|
|
||||||
import com.topjohnwu.magisk.view.dialogs.MagiskInstallDialog;
|
|
||||||
import com.topjohnwu.magisk.view.dialogs.ManagerInstallDialog;
|
|
||||||
import com.topjohnwu.magisk.view.dialogs.UninstallDialog;
|
|
||||||
import com.topjohnwu.net.Networking;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import butterknife.BindColor;
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.OnClick;
|
|
||||||
|
|
||||||
public class MagiskFragment extends BaseFragment implements SwipeRefreshLayout.OnRefreshListener {
|
|
||||||
|
|
||||||
private static boolean shownDialog = false;
|
|
||||||
|
|
||||||
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
|
|
||||||
@BindView(R.id.linearLayout) LinearLayout root;
|
|
||||||
|
|
||||||
@BindView(R.id.install_option_card) CardView installOptionCard;
|
|
||||||
@BindView(R.id.keep_force_enc) CheckBox keepEncChkbox;
|
|
||||||
@BindView(R.id.keep_verity) CheckBox keepVerityChkbox;
|
|
||||||
@BindView(R.id.install_option_expand) ViewGroup optionExpandLayout;
|
|
||||||
@BindView(R.id.arrow) ImageView arrow;
|
|
||||||
|
|
||||||
@BindView(R.id.uninstall_button) CardView uninstallButton;
|
|
||||||
|
|
||||||
@BindColor(R.color.red500) int colorBad;
|
|
||||||
@BindColor(R.color.green500) int colorOK;
|
|
||||||
@BindColor(R.color.yellow500) int colorWarn;
|
|
||||||
@BindColor(R.color.green500) int colorNeutral;
|
|
||||||
@BindColor(R.color.blue500) int colorInfo;
|
|
||||||
|
|
||||||
private UpdateCardHolder magisk;
|
|
||||||
private UpdateCardHolder manager;
|
|
||||||
private SafetyNet safetyNet;
|
|
||||||
private Transition transition;
|
|
||||||
private Expandable optionExpand;
|
|
||||||
|
|
||||||
private void magiskInstall(View v) {
|
|
||||||
// Show Manager update first
|
|
||||||
if (Config.remoteManagerVersionCode > BuildConfig.VERSION_CODE) {
|
|
||||||
new ManagerInstallDialog(requireActivity()).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
new MagiskInstallDialog((BaseActivity) requireActivity()).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void managerInstall(View v) {
|
|
||||||
new ManagerInstallDialog(requireActivity()).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openLink(String url) {
|
|
||||||
Utils.openLink(requireActivity(), Uri.parse(url));
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.paypal)
|
|
||||||
void paypal() {
|
|
||||||
openLink(Const.Url.PAYPAL_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.patreon)
|
|
||||||
void patreon() {
|
|
||||||
openLink(Const.Url.PATREON_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.twitter)
|
|
||||||
void twitter() {
|
|
||||||
openLink(Const.Url.TWITTER_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.github)
|
|
||||||
void github() {
|
|
||||||
openLink(Const.Url.SOURCE_CODE_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.xda)
|
|
||||||
void xda() {
|
|
||||||
openLink(Const.Url.XDA_THREAD);
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.uninstall_button)
|
|
||||||
void uninstall() {
|
|
||||||
new UninstallDialog(requireActivity()).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.arrow)
|
|
||||||
void expandOptions() {
|
|
||||||
if (optionExpand.isExpanded())
|
|
||||||
optionExpand.collapse();
|
|
||||||
else optionExpand.expand();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
|
||||||
@Nullable Bundle savedInstanceState) {
|
|
||||||
View v = inflater.inflate(R.layout.fragment_magisk, container, false);
|
|
||||||
unbinder = new MagiskFragment_ViewBinding(this, v);
|
|
||||||
requireActivity().setTitle(R.string.magisk);
|
|
||||||
|
|
||||||
optionExpand = new ArrowExpandable(new ExpandableViewHolder(optionExpandLayout), arrow);
|
|
||||||
safetyNet = new SafetyNet(v);
|
|
||||||
magisk = new UpdateCardHolder(inflater, root);
|
|
||||||
manager = new UpdateCardHolder(inflater, root);
|
|
||||||
manager.setClickable(vv ->
|
|
||||||
MarkDownWindow.show(requireActivity(), null,
|
|
||||||
getResources().openRawResource(R.raw.changelog)));
|
|
||||||
root.addView(magisk.itemView, 1);
|
|
||||||
root.addView(manager.itemView, 2);
|
|
||||||
|
|
||||||
keepVerityChkbox.setChecked(Config.keepVerity);
|
|
||||||
keepVerityChkbox.setOnCheckedChangeListener((view, checked) -> Config.keepVerity = checked);
|
|
||||||
keepEncChkbox.setChecked(Config.keepEnc);
|
|
||||||
keepEncChkbox.setOnCheckedChangeListener((view, checked) -> Config.keepEnc = checked);
|
|
||||||
|
|
||||||
mSwipeRefreshLayout.setOnRefreshListener(this);
|
|
||||||
|
|
||||||
magisk.install.setOnClickListener(this::magiskInstall);
|
|
||||||
manager.install.setOnClickListener(this::managerInstall);
|
|
||||||
if (Config.get(Config.Key.COREONLY)) {
|
|
||||||
magisk.additional.setText(R.string.core_only_enabled);
|
|
||||||
magisk.additional.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
if (!app.getPackageName().equals(BuildConfig.APPLICATION_ID)) {
|
|
||||||
manager.additional.setText("(" + app.getPackageName() + ")");
|
|
||||||
manager.additional.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
transition = new TransitionSet()
|
|
||||||
.setOrdering(TransitionSet.ORDERING_TOGETHER)
|
|
||||||
.addTransition(new Fade(Fade.OUT))
|
|
||||||
.addTransition(new ChangeBounds())
|
|
||||||
.addTransition(new Fade(Fade.IN));
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
super.onDestroyView();
|
|
||||||
safetyNet.unbinder.unbind();
|
|
||||||
magisk.unbinder.unbind();
|
|
||||||
manager.unbinder.unbind();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRefresh() {
|
|
||||||
mSwipeRefreshLayout.setRefreshing(false);
|
|
||||||
TransitionManager.beginDelayedTransition(root, transition);
|
|
||||||
safetyNet.reset();
|
|
||||||
magisk.reset();
|
|
||||||
manager.reset();
|
|
||||||
|
|
||||||
Config.loadMagiskInfo();
|
|
||||||
updateUI();
|
|
||||||
|
|
||||||
Event.reset(this);
|
|
||||||
Config.remoteMagiskVersionString = null;
|
|
||||||
Config.remoteMagiskVersionCode = -1;
|
|
||||||
|
|
||||||
shownDialog = false;
|
|
||||||
|
|
||||||
// Trigger state check
|
|
||||||
if (Networking.checkNetworkStatus(app)) {
|
|
||||||
CheckUpdates.check();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int[] getListeningEvents() {
|
|
||||||
return new int[] {Event.UPDATE_CHECK_DONE};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEvent(int event) {
|
|
||||||
updateCheckUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUI() {
|
|
||||||
((MainActivity) requireActivity()).checkHideSection();
|
|
||||||
int image, color;
|
|
||||||
String status;
|
|
||||||
if (Config.magiskVersionCode < 0) {
|
|
||||||
color = colorBad;
|
|
||||||
image = R.drawable.ic_cancel;
|
|
||||||
status = getString(R.string.magisk_version_error);
|
|
||||||
magisk.status.setText(status);
|
|
||||||
magisk.currentVersion.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
color = colorOK;
|
|
||||||
image = R.drawable.ic_check_circle;
|
|
||||||
status = getString(R.string.magisk);
|
|
||||||
magisk.currentVersion.setText(getString(R.string.current_installed,
|
|
||||||
String.format(Locale.US, "v%s (%d)",
|
|
||||||
Config.magiskVersionString, Config.magiskVersionCode)));
|
|
||||||
}
|
|
||||||
magisk.statusIcon.setColorFilter(color);
|
|
||||||
magisk.statusIcon.setImageResource(image);
|
|
||||||
|
|
||||||
manager.statusIcon.setColorFilter(colorOK);
|
|
||||||
manager.statusIcon.setImageResource(R.drawable.ic_check_circle);
|
|
||||||
manager.currentVersion.setText(getString(R.string.current_installed,
|
|
||||||
String.format(Locale.US, "v%s (%d)",
|
|
||||||
BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
|
|
||||||
|
|
||||||
if (!Networking.checkNetworkStatus(app)) {
|
|
||||||
// No network, updateCheckUI will not be triggered
|
|
||||||
magisk.status.setText(status);
|
|
||||||
manager.status.setText(R.string.app_name);
|
|
||||||
magisk.setValid(false);
|
|
||||||
manager.setValid(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCheckUI() {
|
|
||||||
int image, color;
|
|
||||||
String status, button = "";
|
|
||||||
|
|
||||||
TransitionManager.beginDelayedTransition(root, transition);
|
|
||||||
|
|
||||||
if (Config.remoteMagiskVersionCode < 0) {
|
|
||||||
color = colorNeutral;
|
|
||||||
image = R.drawable.ic_help;
|
|
||||||
status = getString(R.string.invalid_update_channel);
|
|
||||||
} else {
|
|
||||||
magisk.latestVersion.setText(getString(R.string.latest_version,
|
|
||||||
String.format(Locale.US, "v%s (%d)",
|
|
||||||
Config.remoteMagiskVersionString, Config.remoteMagiskVersionCode)));
|
|
||||||
if (Config.remoteMagiskVersionCode > Config.magiskVersionCode) {
|
|
||||||
color = colorInfo;
|
|
||||||
image = R.drawable.ic_update;
|
|
||||||
status = getString(R.string.magisk_update_title);
|
|
||||||
button = getString(R.string.update);
|
|
||||||
} else {
|
|
||||||
color = colorOK;
|
|
||||||
image = R.drawable.ic_check_circle;
|
|
||||||
status = getString(R.string.magisk_up_to_date);
|
|
||||||
button = getString(R.string.install);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Config.magiskVersionCode > 0) {
|
|
||||||
// Only override status if Magisk is installed
|
|
||||||
magisk.statusIcon.setImageResource(image);
|
|
||||||
magisk.statusIcon.setColorFilter(color);
|
|
||||||
magisk.status.setText(status);
|
|
||||||
magisk.install.setText(button);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Config.remoteManagerVersionCode < 0) {
|
|
||||||
color = colorNeutral;
|
|
||||||
image = R.drawable.ic_help;
|
|
||||||
status = getString(R.string.invalid_update_channel);
|
|
||||||
} else {
|
|
||||||
manager.latestVersion.setText(getString(R.string.latest_version,
|
|
||||||
String.format(Locale.US, "v%s (%d)",
|
|
||||||
Config.remoteManagerVersionString, Config.remoteManagerVersionCode)));
|
|
||||||
if (Config.remoteManagerVersionCode > BuildConfig.VERSION_CODE) {
|
|
||||||
color = colorInfo;
|
|
||||||
image = R.drawable.ic_update;
|
|
||||||
status = getString(R.string.manager_update_title);
|
|
||||||
manager.install.setText(R.string.update);
|
|
||||||
} else {
|
|
||||||
color = colorOK;
|
|
||||||
image = R.drawable.ic_check_circle;
|
|
||||||
status = getString(R.string.manager_up_to_date);
|
|
||||||
manager.install.setText(R.string.install);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
manager.statusIcon.setImageResource(image);
|
|
||||||
manager.statusIcon.setColorFilter(color);
|
|
||||||
manager.status.setText(status);
|
|
||||||
|
|
||||||
magisk.setValid(Config.remoteMagiskVersionCode > 0);
|
|
||||||
manager.setValid(Config.remoteManagerVersionCode > 0);
|
|
||||||
|
|
||||||
if (Config.remoteMagiskVersionCode < 0) {
|
|
||||||
// Hide install related components
|
|
||||||
installOptionCard.setVisibility(View.GONE);
|
|
||||||
uninstallButton.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
// Show install related components
|
|
||||||
installOptionCard.setVisibility(View.VISIBLE);
|
|
||||||
uninstallButton.setVisibility(Shell.rootAccess() ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shownDialog && Config.magiskVersionCode > 0 &&
|
|
||||||
!Shell.su("env_check").exec().isSuccess()) {
|
|
||||||
shownDialog = true;
|
|
||||||
new EnvFixDialog(requireActivity()).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.home
|
||||||
|
|
||||||
|
import com.skoumal.teanity.viewevents.ViewEvent
|
||||||
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
import com.topjohnwu.magisk.Config
|
||||||
|
import com.topjohnwu.magisk.Const
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.databinding.FragmentMagiskBinding
|
||||||
|
import com.topjohnwu.magisk.model.events.*
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskActivity
|
||||||
|
import com.topjohnwu.magisk.utils.ISafetyNetHelper
|
||||||
|
import com.topjohnwu.magisk.view.MarkDownWindow
|
||||||
|
import com.topjohnwu.magisk.view.SafetyNet
|
||||||
|
import com.topjohnwu.magisk.view.SafetyNet.EXT_APK
|
||||||
|
import com.topjohnwu.magisk.view.dialogs.*
|
||||||
|
import com.topjohnwu.net.Networking
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskFragment as NewMagiskFragment
|
||||||
|
|
||||||
|
class MagiskFragment : NewMagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
|
||||||
|
ISafetyNetHelper.Callback {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.fragment_magisk
|
||||||
|
override val viewModel: HomeViewModel by viewModel()
|
||||||
|
|
||||||
|
override fun onResponse(responseCode: Int) = viewModel.finishSafetyNetCheck(responseCode)
|
||||||
|
|
||||||
|
override fun onEventDispatched(event: ViewEvent) {
|
||||||
|
super.onEventDispatched(event)
|
||||||
|
when (event) {
|
||||||
|
is OpenLinkEvent -> openLink(event.url)
|
||||||
|
is ManagerInstallEvent -> installManager()
|
||||||
|
is MagiskInstallEvent -> installMagisk()
|
||||||
|
is UninstallEvent -> uninstall()
|
||||||
|
is ManagerChangelogEvent -> changelogManager()
|
||||||
|
is EnvFixEvent -> fixEnv()
|
||||||
|
is UpdateSafetyNetEvent -> updateSafetyNet(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
requireActivity().setTitle(R.string.magisk)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun installMagisk() {
|
||||||
|
// Show Manager update first
|
||||||
|
if (Config.remoteManagerVersionCode > BuildConfig.VERSION_CODE) {
|
||||||
|
installManager()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
MagiskInstallDialog(requireActivity() as MagiskActivity<*, *>).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun installManager() = ManagerInstallDialog(requireActivity()).show()
|
||||||
|
private fun uninstall() = UninstallDialog(requireActivity()).show()
|
||||||
|
private fun fixEnv() = EnvFixDialog(requireActivity()).show()
|
||||||
|
|
||||||
|
private fun changelogManager() = MarkDownWindow
|
||||||
|
.show(requireActivity(), null, resources.openRawResource(R.raw.changelog))
|
||||||
|
|
||||||
|
private fun downloadSafetyNet(requiresUserInput: Boolean = true) {
|
||||||
|
fun download() = Networking
|
||||||
|
.get(Const.Url.SNET_URL)
|
||||||
|
.getAsFile(EXT_APK) { updateSafetyNet(true) }
|
||||||
|
|
||||||
|
if (!requiresUserInput) {
|
||||||
|
download()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomAlertDialog(requireActivity())
|
||||||
|
.setTitle(R.string.proprietary_title)
|
||||||
|
.setMessage(R.string.proprietary_notice)
|
||||||
|
.setCancelable(false)
|
||||||
|
.setPositiveButton(R.string.yes) { _, _ -> download() }
|
||||||
|
.setNegativeButton(R.string.no_thanks) { _, _ -> viewModel.finishSafetyNetCheck(-2) }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSafetyNet(dieOnError: Boolean) {
|
||||||
|
try {
|
||||||
|
SafetyNet.dyRun(requireActivity(), this)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (dieOnError) {
|
||||||
|
viewModel.finishSafetyNetCheck(-1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Shell.sh("rm -rf " + EXT_APK.parent).exec()
|
||||||
|
EXT_APK.parentFile?.mkdir()
|
||||||
|
downloadSafetyNet(!dieOnError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.home
|
||||||
|
|
||||||
|
|
||||||
|
enum class MagiskItem {
|
||||||
|
MANAGER, MAGISK
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.home
|
||||||
|
|
||||||
|
|
||||||
|
enum class MagiskState {
|
||||||
|
NO_ROOT, NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.home
|
||||||
|
|
||||||
|
enum class SafetyNetState {
|
||||||
|
LOADING, PASS, FAILED, IDLE
|
||||||
|
}
|
@ -1,47 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.log;
|
|
||||||
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.model.adapters.TabFragmentAdapter;
|
|
||||||
import com.topjohnwu.magisk.ui.MainActivity;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseFragment;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class LogFragment extends BaseFragment {
|
|
||||||
|
|
||||||
@BindView(R.id.container) ViewPager viewPager;
|
|
||||||
@BindView(R.id.tab) TabLayout tab;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState) {
|
|
||||||
// Inflate the layout for this fragment
|
|
||||||
View v = inflater.inflate(R.layout.fragment_log, container, false);
|
|
||||||
unbinder = new LogFragment_ViewBinding(this, v);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
((MainActivity) requireActivity()).toolbar.setElevation(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
TabFragmentAdapter adapter = new TabFragmentAdapter(getChildFragmentManager());
|
|
||||||
|
|
||||||
adapter.addTab(new SuLogFragment(), getString(R.string.superuser));
|
|
||||||
adapter.addTab(new MagiskLogFragment(), getString(R.string.magisk));
|
|
||||||
tab.setupWithViewPager(viewPager);
|
|
||||||
tab.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
viewPager.setAdapter(adapter);
|
|
||||||
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}
|
|
53
app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.kt
Normal file
53
app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.kt
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.log
|
||||||
|
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import com.skoumal.teanity.viewevents.ViewEvent
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.databinding.FragmentLogBinding
|
||||||
|
import com.topjohnwu.magisk.model.events.PageChangedEvent
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskFragment
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
|
class LogFragment : MagiskFragment<LogViewModel, FragmentLogBinding>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.fragment_log
|
||||||
|
override val viewModel: LogViewModel by viewModel()
|
||||||
|
|
||||||
|
override fun onEventDispatched(event: ViewEvent) {
|
||||||
|
super.onEventDispatched(event)
|
||||||
|
when (event) {
|
||||||
|
is PageChangedEvent -> magiskActivity.invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
binding.logTabs.setupWithViewPager(binding.logContainer, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
magiskActivity.setTitle(R.string.log)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.menu_log, menu)
|
||||||
|
menu.findItem(R.id.menu_save).isVisible = viewModel.currentPage.value == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.menu_save -> viewModel.saveLog()
|
||||||
|
R.id.menu_clear -> viewModel.clearLog()
|
||||||
|
R.id.menu_refresh -> viewModel.refresh()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
122
app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt
Normal file
122
app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.log
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.databinding.ObservableArrayList
|
||||||
|
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||||
|
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||||
|
import com.skoumal.teanity.extensions.doOnSuccessUi
|
||||||
|
import com.skoumal.teanity.extensions.subscribeK
|
||||||
|
import com.skoumal.teanity.util.DiffObservableList
|
||||||
|
import com.skoumal.teanity.util.KObservableField
|
||||||
|
import com.skoumal.teanity.viewevents.SnackbarEvent
|
||||||
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.Const
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.data.database.MagiskDB
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.*
|
||||||
|
import com.topjohnwu.magisk.model.events.PageChangedEvent
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||||
|
import com.topjohnwu.magisk.utils.toSingle
|
||||||
|
import com.topjohnwu.magisk.utils.zip
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import io.reactivex.Single
|
||||||
|
import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter
|
||||||
|
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class LogViewModel(
|
||||||
|
private val resources: Resources,
|
||||||
|
private val database: MagiskDB
|
||||||
|
) : MagiskViewModel(), BindingViewPagerAdapter.PageTitles<ComparableRvItem<*>> {
|
||||||
|
|
||||||
|
val items = DiffObservableList(ComparableRvItem.callback)
|
||||||
|
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
|
||||||
|
item.bind(itemBinding)
|
||||||
|
itemBinding.bindExtra(BR.viewModel, this@LogViewModel)
|
||||||
|
}
|
||||||
|
val currentPage = KObservableField(0)
|
||||||
|
private val currentItem get() = items[currentPage.value]
|
||||||
|
|
||||||
|
private val logItem get() = items[0] as LogRvItem
|
||||||
|
private val magiskLogItem get() = items[1] as MagiskLogRvItem
|
||||||
|
|
||||||
|
init {
|
||||||
|
currentPage.addOnPropertyChangedCallback {
|
||||||
|
it ?: return@addOnPropertyChangedCallback
|
||||||
|
PageChangedEvent().publish()
|
||||||
|
}
|
||||||
|
|
||||||
|
items.addAll(listOf(LogRvItem(), MagiskLogRvItem()))
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPageTitle(position: Int, item: ComparableRvItem<*>?) = when (item) {
|
||||||
|
is LogRvItem -> resources.getString(R.string.superuser)
|
||||||
|
is MagiskLogRvItem -> resources.getString(R.string.magisk)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() = zip(updateLogs(), updateMagiskLog()) { _, _ -> true }
|
||||||
|
.subscribeK()
|
||||||
|
.add()
|
||||||
|
|
||||||
|
fun saveLog() {
|
||||||
|
val now = Calendar.getInstance()
|
||||||
|
val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format(
|
||||||
|
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
|
||||||
|
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
|
||||||
|
now.get(Calendar.MINUTE), now.get(Calendar.SECOND)
|
||||||
|
)
|
||||||
|
|
||||||
|
val logFile = File(Const.EXTERNAL_PATH, filename)
|
||||||
|
try {
|
||||||
|
logFile.createNewFile()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Shell.su("cat ${Const.MAGISK_LOG} > $logFile").submit {
|
||||||
|
SnackbarEvent(logFile.path).publish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearLog() = when (currentItem) {
|
||||||
|
is LogRvItem -> clearLogs { refresh() }
|
||||||
|
is MagiskLogRvItem -> clearMagiskLogs { refresh() }
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearLogs(callback: () -> Unit) {
|
||||||
|
Single.fromCallable { database.clearLogs() }
|
||||||
|
.subscribeK {
|
||||||
|
SnackbarEvent(R.string.logs_cleared).publish()
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
.add()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearMagiskLogs(callback: () -> Unit) {
|
||||||
|
Shell.su("echo -n > " + Const.MAGISK_LOG).submit {
|
||||||
|
SnackbarEvent(R.string.logs_cleared).publish()
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateLogs() = Single.fromCallable { database.logs }
|
||||||
|
.flattenAsFlowable { it }
|
||||||
|
.map { it.map { LogItemEntryRvItem(it) } }
|
||||||
|
.map { LogItemRvItem(ObservableArrayList<ComparableRvItem<*>>().apply { addAll(it) }) }
|
||||||
|
.toList()
|
||||||
|
.doOnSuccessUi { logItem.update(it) }
|
||||||
|
|
||||||
|
private fun updateMagiskLog() = Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").toSingle()
|
||||||
|
.map { it.exec() }
|
||||||
|
.map { it.out }
|
||||||
|
.flattenAsFlowable { it }
|
||||||
|
.map { ConsoleRvItem(it) }
|
||||||
|
.toList()
|
||||||
|
.doOnSuccessUi { magiskLogItem.update(it) }
|
||||||
|
|
||||||
|
}
|
@ -1,157 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.log;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.model.adapters.StringListAdapter;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseFragment;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.magisk.view.SnackbarMaker;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
import com.topjohnwu.superuser.internal.NOPList;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class MagiskLogFragment extends BaseFragment {
|
|
||||||
|
|
||||||
@BindView(R.id.recyclerView) RecyclerView rv;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
View view = inflater.inflate(R.layout.fragment_magisk_log, container, false);
|
|
||||||
unbinder = new MagiskLogFragment_ViewBinding(this, view);
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
getActivity().setTitle(R.string.log);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
readLogs();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
|
||||||
inflater.inflate(R.menu.menu_log, menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.menu_refresh:
|
|
||||||
readLogs();
|
|
||||||
return true;
|
|
||||||
case R.id.menu_save:
|
|
||||||
runWithExternalRW(this::saveLogs);
|
|
||||||
return true;
|
|
||||||
case R.id.menu_clear:
|
|
||||||
clearLogs();
|
|
||||||
rv.setAdapter(new MagiskLogAdapter(NOPList.getInstance()));
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void readLogs() {
|
|
||||||
Shell.su("tail -n 5000 " + Const.MAGISK_LOG).submit(result -> {
|
|
||||||
rv.setAdapter(new MagiskLogAdapter(result.getOut()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveLogs() {
|
|
||||||
Calendar now = Calendar.getInstance();
|
|
||||||
String filename = Utils.fmt("magisk_log_%04d%02d%02d_%02d%02d%02d.log",
|
|
||||||
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
|
|
||||||
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
|
|
||||||
now.get(Calendar.MINUTE), now.get(Calendar.SECOND));
|
|
||||||
|
|
||||||
File logFile = new File(Const.EXTERNAL_PATH, filename);
|
|
||||||
try {
|
|
||||||
logFile.createNewFile();
|
|
||||||
} catch (IOException e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Shell.su("cat " + Const.MAGISK_LOG + " > " + logFile)
|
|
||||||
.submit(result ->
|
|
||||||
SnackbarMaker.make(rv, logFile.getPath(), Snackbar.LENGTH_SHORT).show());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearLogs() {
|
|
||||||
Shell.su("echo -n > " + Const.MAGISK_LOG).submit();
|
|
||||||
SnackbarMaker.make(rv, R.string.logs_cleared, Snackbar.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MagiskLogAdapter extends StringListAdapter<MagiskLogAdapter.ViewHolder> {
|
|
||||||
|
|
||||||
MagiskLogAdapter(List<String> list) {
|
|
||||||
super(list);
|
|
||||||
if (mList.isEmpty())
|
|
||||||
mList.add(requireContext().getString(R.string.log_is_empty));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int itemLayoutRes() {
|
|
||||||
return R.layout.list_item_console;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ViewHolder createViewHolder(@NonNull View v) {
|
|
||||||
return new ViewHolder(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onUpdateTextWidth(ViewHolder holder) {
|
|
||||||
super.onUpdateTextWidth(holder);
|
|
||||||
// Find the longest string and update accordingly
|
|
||||||
int max = 0;
|
|
||||||
String maxStr = "";
|
|
||||||
for (String s : mList) {
|
|
||||||
int len = s.length();
|
|
||||||
if (len > max) {
|
|
||||||
max = len;
|
|
||||||
maxStr = s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
holder.txt.setText(maxStr);
|
|
||||||
super.onUpdateTextWidth(holder);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ViewHolder extends StringListAdapter.ViewHolder {
|
|
||||||
|
|
||||||
public ViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int textViewResId() {
|
|
||||||
return R.id.txt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.log;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.model.adapters.SuLogAdapter;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseFragment;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class SuLogFragment extends BaseFragment {
|
|
||||||
|
|
||||||
@BindView(R.id.empty_rv) TextView emptyRv;
|
|
||||||
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
|
||||||
|
|
||||||
private SuLogAdapter adapter;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
|
||||||
inflater.inflate(R.menu.menu_log, menu);
|
|
||||||
menu.findItem(R.id.menu_save).setVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState) {
|
|
||||||
// Inflate the layout for this fragment
|
|
||||||
View v = inflater.inflate(R.layout.fragment_su_log, container, false);
|
|
||||||
unbinder = new SuLogFragment_ViewBinding(this, v);
|
|
||||||
adapter = new SuLogAdapter(app.mDB);
|
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
|
|
||||||
updateList();
|
|
||||||
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateList() {
|
|
||||||
adapter.notifyDBChanged();
|
|
||||||
|
|
||||||
if (adapter.getSectionCount() == 0) {
|
|
||||||
emptyRv.setVisibility(View.VISIBLE);
|
|
||||||
recyclerView.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
emptyRv.setVisibility(View.GONE);
|
|
||||||
recyclerView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.menu_refresh:
|
|
||||||
updateList();
|
|
||||||
return true;
|
|
||||||
case R.id.menu_clear:
|
|
||||||
app.mDB.clearLogs();
|
|
||||||
updateList();
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,141 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.module
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.database.Cursor
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||||
|
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||||
|
import com.skoumal.teanity.extensions.subscribeK
|
||||||
|
import com.skoumal.teanity.util.DiffObservableList
|
||||||
|
import com.skoumal.teanity.util.KObservableField
|
||||||
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
|
||||||
|
import com.topjohnwu.magisk.model.entity.Module
|
||||||
|
import com.topjohnwu.magisk.model.entity.Repo
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.SectionRvItem
|
||||||
|
import com.topjohnwu.magisk.model.events.InstallModuleEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
|
||||||
|
import com.topjohnwu.magisk.tasks.UpdateRepos
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||||
|
import com.topjohnwu.magisk.utils.Event
|
||||||
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
|
import com.topjohnwu.magisk.utils.toSingle
|
||||||
|
import com.topjohnwu.magisk.utils.update
|
||||||
|
import io.reactivex.Single
|
||||||
|
import io.reactivex.disposables.Disposable
|
||||||
|
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||||
|
|
||||||
|
class ModuleViewModel(
|
||||||
|
private val repoDatabase: RepoDatabaseHelper,
|
||||||
|
private val resources: Resources
|
||||||
|
) : MagiskViewModel() {
|
||||||
|
|
||||||
|
val query = KObservableField("")
|
||||||
|
|
||||||
|
private val allItems = mutableListOf<ComparableRvItem<*>>()
|
||||||
|
|
||||||
|
val itemsInstalled = DiffObservableList(ComparableRvItem.callback)
|
||||||
|
val itemsRemote = DiffObservableList(ComparableRvItem.callback)
|
||||||
|
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
|
||||||
|
item.bind(itemBinding)
|
||||||
|
itemBinding.bindExtra(BR.viewModel, this@ModuleViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var queryDisposable: Disposable? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
query.addOnPropertyChangedCallback {
|
||||||
|
queryDisposable?.dispose()
|
||||||
|
queryDisposable = query()
|
||||||
|
}
|
||||||
|
Event.register(this)
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getListeningEvents(): IntArray {
|
||||||
|
return intArrayOf(Event.MODULE_LOAD_DONE, Event.REPO_LOAD_DONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEvent(event: Int) = when (event) {
|
||||||
|
Event.MODULE_LOAD_DONE -> updateModules(Event.getResult(event))
|
||||||
|
Event.REPO_LOAD_DONE -> updateRepos()
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fabPressed() = OpenFilePickerEvent().publish()
|
||||||
|
fun repoPressed(item: RepoRvItem) = OpenChangelogEvent(item.item).publish()
|
||||||
|
fun downloadPressed(item: RepoRvItem) = InstallModuleEvent(item.item).publish()
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
state = State.LOADING
|
||||||
|
Utils.loadModules(true)
|
||||||
|
UpdateRepos().exec(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateModules(result: Map<String, Module>) = result.values
|
||||||
|
.map { ModuleRvItem(it) }
|
||||||
|
.let { itemsInstalled.update(it) }
|
||||||
|
|
||||||
|
internal fun updateRepos() {
|
||||||
|
Single.fromCallable { repoDatabase.repoCursor.toList { Repo(it) } }
|
||||||
|
.flattenAsFlowable { it }
|
||||||
|
.map { RepoRvItem(it) }
|
||||||
|
.toList()
|
||||||
|
.doOnSuccess { allItems.update(it) }
|
||||||
|
.flatMap { queryRaw() }
|
||||||
|
.applyViewModel(this)
|
||||||
|
.subscribeK { itemsRemote.update(it.first, it.second) }
|
||||||
|
.add()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun query() = queryRaw()
|
||||||
|
.subscribeK { itemsRemote.update(it.first, it.second) }
|
||||||
|
|
||||||
|
private fun queryRaw(query: String = this.query.value) = allItems.toSingle()
|
||||||
|
.map { it.filterIsInstance<RepoRvItem>() }
|
||||||
|
.flattenAsFlowable { it }
|
||||||
|
.filter {
|
||||||
|
it.item.name.contains(query, ignoreCase = true) ||
|
||||||
|
it.item.author.contains(query, ignoreCase = true) ||
|
||||||
|
it.item.description.contains(query, ignoreCase = true)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
.map { if (query.isEmpty()) it.divide() else it }
|
||||||
|
.map { it to itemsRemote.calculateDiff(it) }
|
||||||
|
|
||||||
|
private fun List<RepoRvItem>.divide(): List<ComparableRvItem<*>> {
|
||||||
|
val installed = itemsInstalled.filterIsInstance<ModuleRvItem>()
|
||||||
|
val installedModules = filter { installed.any { item -> it.item.id == item.item.id } }
|
||||||
|
|
||||||
|
fun installedByID(id: String) = installed.firstOrNull { it.item.id == id }
|
||||||
|
|
||||||
|
fun List<RepoRvItem>.filterObsolete() = filter {
|
||||||
|
val module = installedByID(it.item.id) ?: return@filter false
|
||||||
|
module.item.versionCode != it.item.versionCode
|
||||||
|
}
|
||||||
|
|
||||||
|
val resultObsolete = installedModules.filterObsolete()
|
||||||
|
val resultInstalled = installedModules - resultObsolete
|
||||||
|
val resultRemote = toList() - installedModules
|
||||||
|
|
||||||
|
fun buildList(@StringRes text: Int, list: List<RepoRvItem>): List<ComparableRvItem<*>> {
|
||||||
|
return if (list.isEmpty()) list
|
||||||
|
else listOf(SectionRvItem(resources.getString(text))) + list
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildList(R.string.update_available, resultObsolete) +
|
||||||
|
buildList(R.string.installed, resultInstalled) +
|
||||||
|
buildList(R.string.not_installed, resultRemote)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
|
||||||
|
val out = mutableListOf<Result>()
|
||||||
|
while (moveToNext()) out.add(transformer(this))
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,142 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.module;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.ClassMap;
|
|
||||||
import com.topjohnwu.magisk.Const;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.model.adapters.ModulesAdapter;
|
|
||||||
import com.topjohnwu.magisk.model.entity.Module;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseFragment;
|
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashActivity;
|
|
||||||
import com.topjohnwu.magisk.utils.Event;
|
|
||||||
import com.topjohnwu.magisk.utils.RootUtils;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
import com.topjohnwu.superuser.Shell;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.OnClick;
|
|
||||||
|
|
||||||
public class ModulesFragment extends BaseFragment {
|
|
||||||
|
|
||||||
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
|
|
||||||
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
|
||||||
@BindView(R.id.empty_rv) TextView emptyRv;
|
|
||||||
|
|
||||||
@OnClick(R.id.fab)
|
|
||||||
void selectFile() {
|
|
||||||
runWithExternalRW(() -> {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
|
||||||
intent.setType("application/zip");
|
|
||||||
startActivityForResult(intent, Const.ID.FETCH_ZIP);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Module> listModules = new ArrayList<>();
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
|
||||||
View view = inflater.inflate(R.layout.fragment_modules, container, false);
|
|
||||||
unbinder = new ModulesFragment_ViewBinding(this, view);
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
|
|
||||||
mSwipeRefreshLayout.setOnRefreshListener(() -> {
|
|
||||||
recyclerView.setVisibility(View.GONE);
|
|
||||||
Utils.loadModules();
|
|
||||||
});
|
|
||||||
|
|
||||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
|
||||||
@Override
|
|
||||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
|
||||||
mSwipeRefreshLayout.setEnabled(recyclerView.getChildAt(0).getTop() >= 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
|
|
||||||
super.onScrollStateChanged(recyclerView, newState);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
requireActivity().setTitle(R.string.modules);
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int[] getListeningEvents() {
|
|
||||||
return new int[] {Event.MODULE_LOAD_DONE};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEvent(int event) {
|
|
||||||
updateUI(Event.getResult(event));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) {
|
|
||||||
// Get the URI of the selected file
|
|
||||||
Intent intent = new Intent(getActivity(), ClassMap.get(FlashActivity.class));
|
|
||||||
intent.setData(data.getData()).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
|
||||||
inflater.inflate(R.menu.menu_reboot, menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.reboot:
|
|
||||||
RootUtils.reboot();
|
|
||||||
return true;
|
|
||||||
case R.id.reboot_recovery:
|
|
||||||
Shell.su("/system/bin/reboot recovery").submit();
|
|
||||||
return true;
|
|
||||||
case R.id.reboot_bootloader:
|
|
||||||
Shell.su("/system/bin/reboot bootloader").submit();
|
|
||||||
return true;
|
|
||||||
case R.id.reboot_download:
|
|
||||||
Shell.su("/system/bin/reboot download").submit();
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUI(Map<String, Module> moduleMap) {
|
|
||||||
listModules.clear();
|
|
||||||
listModules.addAll(moduleMap.values());
|
|
||||||
if (listModules.size() == 0) {
|
|
||||||
emptyRv.setVisibility(View.VISIBLE);
|
|
||||||
recyclerView.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
emptyRv.setVisibility(View.GONE);
|
|
||||||
recyclerView.setVisibility(View.VISIBLE);
|
|
||||||
recyclerView.setAdapter(new ModulesAdapter(listModules));
|
|
||||||
}
|
|
||||||
mSwipeRefreshLayout.setRefreshing(false);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,115 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.module
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.skoumal.teanity.viewevents.ViewEvent
|
||||||
|
import com.topjohnwu.magisk.ClassMap
|
||||||
|
import com.topjohnwu.magisk.Const
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.databinding.FragmentModulesBinding
|
||||||
|
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskFragment
|
||||||
|
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
||||||
|
import com.topjohnwu.magisk.utils.RootUtils
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
|
||||||
|
class ModulesFragment : MagiskFragment<ModuleViewModel, FragmentModulesBinding>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.fragment_modules
|
||||||
|
override val viewModel: ModuleViewModel by sharedViewModel()
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) {
|
||||||
|
// Get the URI of the selected file
|
||||||
|
val intent = Intent(activity, ClassMap.get<Any>(FlashActivity::class.java))
|
||||||
|
intent.setData(data.data).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.modulesContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
binding.modulesRefreshLayout.isEnabled = recyclerView.getChildAt(0).top >= 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEventDispatched(event: ViewEvent) {
|
||||||
|
super.onEventDispatched(event)
|
||||||
|
when (event) {
|
||||||
|
is OpenFilePickerEvent -> selectFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
requireActivity().setTitle(R.string.modules)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.menu_reboot, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.reboot -> {
|
||||||
|
RootUtils.reboot()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.reboot_recovery -> {
|
||||||
|
Shell.su("/system/bin/reboot recovery").submit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.reboot_bootloader -> {
|
||||||
|
Shell.su("/system/bin/reboot bootloader").submit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.reboot_download -> {
|
||||||
|
Shell.su("/system/bin/reboot download").submit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectFile() {
|
||||||
|
magiskActivity.runWithExternalRW {
|
||||||
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
|
intent.type = "application/zip"
|
||||||
|
startActivityForResult(intent, Const.ID.FETCH_ZIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*override fun getListeningEvents(): IntArray {
|
||||||
|
return intArrayOf(Event.MODULE_LOAD_DONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEvent(event: Int) {
|
||||||
|
updateUI(Event.getResult(event))
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/*private fun updateUI(moduleMap: Map<String, Module>) {
|
||||||
|
listModules.clear()
|
||||||
|
listModules.addAll(moduleMap.values)
|
||||||
|
if (listModules.size == 0) {
|
||||||
|
emptyRv!!.visibility = View.VISIBLE
|
||||||
|
recyclerView!!.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
emptyRv!!.visibility = View.GONE
|
||||||
|
recyclerView!!.visibility = View.VISIBLE
|
||||||
|
recyclerView!!.adapter = ModulesAdapter(listModules)
|
||||||
|
}
|
||||||
|
mSwipeRefreshLayout!!.isRefreshing = false
|
||||||
|
}*/
|
||||||
|
}
|
@ -1,102 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.module;
|
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.SearchView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.Config;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.model.adapters.ReposAdapter;
|
|
||||||
import com.topjohnwu.magisk.tasks.UpdateRepos;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseFragment;
|
|
||||||
import com.topjohnwu.magisk.utils.Event;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class ReposFragment extends BaseFragment {
|
|
||||||
|
|
||||||
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
|
||||||
@BindView(R.id.empty_rv) TextView emptyRv;
|
|
||||||
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
|
|
||||||
|
|
||||||
private ReposAdapter adapter;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
|
||||||
View view = inflater.inflate(R.layout.fragment_repos, container, false);
|
|
||||||
unbinder = new ReposFragment_ViewBinding(this, view);
|
|
||||||
|
|
||||||
mSwipeRefreshLayout.setRefreshing(true);
|
|
||||||
mSwipeRefreshLayout.setOnRefreshListener(() -> new UpdateRepos().exec(true));
|
|
||||||
|
|
||||||
adapter = new ReposAdapter();
|
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
recyclerView.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
requireActivity().setTitle(R.string.downloads);
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
super.onDestroyView();
|
|
||||||
Event.unregister(adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int[] getListeningEvents() {
|
|
||||||
return new int[] {Event.REPO_LOAD_DONE};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEvent(int event) {
|
|
||||||
adapter.notifyDBChanged(false);
|
|
||||||
Event.register(adapter);
|
|
||||||
mSwipeRefreshLayout.setRefreshing(false);
|
|
||||||
boolean empty = adapter.getItemCount() == 0;
|
|
||||||
recyclerView.setVisibility(empty ? View.GONE : View.VISIBLE);
|
|
||||||
emptyRv.setVisibility(empty ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
|
||||||
inflater.inflate(R.menu.menu_repo, menu);
|
|
||||||
SearchView search = (SearchView) menu.findItem(R.id.repo_search).getActionView();
|
|
||||||
adapter.setSearchView(search);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
if (item.getItemId() == R.id.repo_sort) {
|
|
||||||
new AlertDialog.Builder(getActivity())
|
|
||||||
.setTitle(R.string.sorting_order)
|
|
||||||
.setSingleChoiceItems(R.array.sorting_orders,
|
|
||||||
Config.get(Config.Key.REPO_ORDER), (d, which) -> {
|
|
||||||
Config.set(Config.Key.REPO_ORDER, which);
|
|
||||||
adapter.notifyDBChanged(true);
|
|
||||||
d.dismiss();
|
|
||||||
}).show();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,104 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.module
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.widget.SearchView
|
||||||
|
import com.skoumal.teanity.viewevents.ViewEvent
|
||||||
|
import com.topjohnwu.magisk.ClassMap
|
||||||
|
import com.topjohnwu.magisk.Config
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.databinding.FragmentReposBinding
|
||||||
|
import com.topjohnwu.magisk.model.download.DownloadModuleService
|
||||||
|
import com.topjohnwu.magisk.model.entity.Repo
|
||||||
|
import com.topjohnwu.magisk.model.events.InstallModuleEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskFragment
|
||||||
|
import com.topjohnwu.magisk.view.MarkDownWindow
|
||||||
|
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
|
||||||
|
class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
|
||||||
|
SearchView.OnQueryTextListener {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.fragment_repos
|
||||||
|
override val viewModel: ModuleViewModel by sharedViewModel()
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
requireActivity().setTitle(R.string.downloads)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEventDispatched(event: ViewEvent) {
|
||||||
|
super.onEventDispatched(event)
|
||||||
|
when (event) {
|
||||||
|
is OpenChangelogEvent -> openChangelog(event.item)
|
||||||
|
is InstallModuleEvent -> installModule(event.item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.menu_repo, menu)
|
||||||
|
(menu.findItem(R.id.repo_search).actionView as? SearchView)
|
||||||
|
?.setOnQueryTextListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
if (item.itemId == R.id.repo_sort) {
|
||||||
|
AlertDialog.Builder(activity)
|
||||||
|
.setTitle(R.string.sorting_order)
|
||||||
|
.setSingleChoiceItems(
|
||||||
|
R.array.sorting_orders,
|
||||||
|
Config.get<Int>(Config.Key.REPO_ORDER)!!
|
||||||
|
) { d, which ->
|
||||||
|
Config.set(Config.Key.REPO_ORDER, which)
|
||||||
|
viewModel.updateRepos()
|
||||||
|
d.dismiss()
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextSubmit(p0: String?): Boolean {
|
||||||
|
viewModel.query.value = p0.orEmpty()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(p0: String?): Boolean {
|
||||||
|
viewModel.query.value = p0.orEmpty()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openChangelog(item: Repo) {
|
||||||
|
MarkDownWindow.show(context, null, item.detailUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun installModule(item: Repo) {
|
||||||
|
val context = magiskActivity
|
||||||
|
|
||||||
|
fun download(install: Boolean) {
|
||||||
|
context.runWithExternalRW {
|
||||||
|
val intent = Intent(activity, ClassMap.get<Any>(DownloadModuleService::class.java))
|
||||||
|
.putExtra("repo", item).putExtra("install", install)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.startForegroundService(intent) //hmm, service starts itself in foreground, this seems unnecessary
|
||||||
|
} else {
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomAlertDialog(context)
|
||||||
|
.setTitle(context.getString(R.string.repo_install_title, item.name))
|
||||||
|
.setMessage(context.getString(R.string.repo_install_msg, item.downloadFilename))
|
||||||
|
.setCancelable(true)
|
||||||
|
.setPositiveButton(R.string.install) { _, _ -> download(true) }
|
||||||
|
.setNeutralButton(R.string.download) { _, _ -> download(false) }
|
||||||
|
.setNegativeButton(R.string.no_thanks, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
@ -8,13 +8,6 @@ import android.view.View;
|
|||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.preference.ListPreference;
|
|
||||||
import androidx.preference.Preference;
|
|
||||||
import androidx.preference.PreferenceCategory;
|
|
||||||
import androidx.preference.PreferenceScreen;
|
|
||||||
import androidx.preference.SwitchPreferenceCompat;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.BuildConfig;
|
import com.topjohnwu.magisk.BuildConfig;
|
||||||
import com.topjohnwu.magisk.Config;
|
import com.topjohnwu.magisk.Config;
|
||||||
import com.topjohnwu.magisk.Const;
|
import com.topjohnwu.magisk.Const;
|
||||||
@ -35,18 +28,32 @@ import java.io.IOException;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.preference.ListPreference;
|
||||||
|
import androidx.preference.Preference;
|
||||||
|
import androidx.preference.PreferenceCategory;
|
||||||
|
import androidx.preference.PreferenceScreen;
|
||||||
|
import androidx.preference.SwitchPreferenceCompat;
|
||||||
|
|
||||||
public class SettingsFragment extends BasePreferenceFragment {
|
public class SettingsFragment extends BasePreferenceFragment {
|
||||||
|
|
||||||
private ListPreference updateChannel, autoRes, suNotification,
|
private ListPreference updateChannel, autoRes, suNotification,
|
||||||
requestTimeout, rootConfig, multiuserConfig, nsConfig;
|
requestTimeout, rootConfig, multiuserConfig, nsConfig;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
public void onStart() {
|
||||||
setPreferencesFromResource(R.xml.app_settings, rootKey);
|
super.onStart();
|
||||||
|
setHasOptionsMenu(true);
|
||||||
requireActivity().setTitle(R.string.settings);
|
requireActivity().setTitle(R.string.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
getPreferenceManager().setStorageDeviceProtected();
|
||||||
|
setPreferencesFromResource(R.xml.app_settings, rootKey);
|
||||||
|
|
||||||
boolean showSuperuser = Utils.showSuperUser();
|
boolean showSuperuser = Utils.showSuperUser();
|
||||||
app.prefs.edit()
|
app.getPrefs().edit()
|
||||||
.putBoolean(Config.Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
|
.putBoolean(Config.Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
|
||||||
.apply();
|
.apply();
|
||||||
|
|
||||||
@ -66,8 +73,8 @@ public class SettingsFragment extends BasePreferenceFragment {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
findPreference("clear").setOnPreferenceClickListener(pref -> {
|
findPreference("clear").setOnPreferenceClickListener(pref -> {
|
||||||
app.prefs.edit().remove(Config.Key.ETAG_KEY).apply();
|
app.getPrefs().edit().remove(Config.Key.ETAG_KEY).apply();
|
||||||
app.repoDB.clearRepo();
|
app.getRepoDB().clearRepo();
|
||||||
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT);
|
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@ -94,12 +101,12 @@ public class SettingsFragment extends BasePreferenceFragment {
|
|||||||
if (channel == Config.Value.CUSTOM_CHANNEL) {
|
if (channel == Config.Value.CUSTOM_CHANNEL) {
|
||||||
View v = LayoutInflater.from(requireActivity()).inflate(R.layout.custom_channel_dialog, null);
|
View v = LayoutInflater.from(requireActivity()).inflate(R.layout.custom_channel_dialog, null);
|
||||||
EditText url = v.findViewById(R.id.custom_url);
|
EditText url = v.findViewById(R.id.custom_url);
|
||||||
url.setText(app.prefs.getString(Config.Key.CUSTOM_CHANNEL, ""));
|
url.setText(app.getPrefs().getString(Config.Key.CUSTOM_CHANNEL, ""));
|
||||||
new AlertDialog.Builder(requireActivity())
|
new AlertDialog.Builder(requireActivity())
|
||||||
.setTitle(R.string.settings_update_custom)
|
.setTitle(R.string.settings_update_custom)
|
||||||
.setView(v)
|
.setView(v)
|
||||||
.setPositiveButton(R.string.ok, (d, i) ->
|
.setPositiveButton(R.string.ok, (d, i) ->
|
||||||
Config.set(Config.Key.CUSTOM_CHANNEL, url.getText().toString()))
|
Config.set(Config.Key.CUSTOM_CHANNEL, url.getText().toString()))
|
||||||
.setNegativeButton(R.string.close, (d, i) ->
|
.setNegativeButton(R.string.close, (d, i) ->
|
||||||
Config.set(Config.Key.UPDATE_CHANNEL, prev))
|
Config.set(Config.Key.UPDATE_CHANNEL, prev))
|
||||||
.setOnCancelListener(d ->
|
.setOnCancelListener(d ->
|
||||||
@ -183,7 +190,7 @@ public class SettingsFragment extends BasePreferenceFragment {
|
|||||||
case Config.Key.ROOT_ACCESS:
|
case Config.Key.ROOT_ACCESS:
|
||||||
case Config.Key.SU_MULTIUSER_MODE:
|
case Config.Key.SU_MULTIUSER_MODE:
|
||||||
case Config.Key.SU_MNT_NS:
|
case Config.Key.SU_MNT_NS:
|
||||||
app.mDB.setSettings(key, Utils.getPrefsInt(prefs, key));
|
app.getDB().setSettings(key, Utils.getPrefsInt(prefs, key));
|
||||||
break;
|
break;
|
||||||
case Config.Key.DARK_THEME:
|
case Config.Key.DARK_THEME:
|
||||||
requireActivity().recreate();
|
requireActivity().recreate();
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.superuser;
|
|
||||||
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.model.adapters.PolicyAdapter;
|
|
||||||
import com.topjohnwu.magisk.model.entity.Policy;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseFragment;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
|
|
||||||
public class SuperuserFragment extends BaseFragment {
|
|
||||||
|
|
||||||
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
|
||||||
@BindView(R.id.empty_rv) TextView emptyRv;
|
|
||||||
|
|
||||||
private PackageManager pm;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
|
||||||
View view = inflater.inflate(R.layout.fragment_superuser, container, false);
|
|
||||||
unbinder = new SuperuserFragment_ViewBinding(this, view);
|
|
||||||
|
|
||||||
pm = requireActivity().getPackageManager();
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
requireActivity().setTitle(getString(R.string.superuser));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
displayPolicyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displayPolicyList() {
|
|
||||||
List<Policy> policyList = app.mDB.getPolicyList();
|
|
||||||
|
|
||||||
if (policyList.size() == 0) {
|
|
||||||
emptyRv.setVisibility(View.VISIBLE);
|
|
||||||
recyclerView.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
recyclerView.setAdapter(new PolicyAdapter(policyList, app.mDB, pm));
|
|
||||||
emptyRv.setVisibility(View.GONE);
|
|
||||||
recyclerView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,25 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.superuser
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.databinding.FragmentSuperuserBinding
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskFragment
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
|
class SuperuserFragment :
|
||||||
|
MagiskFragment<SuperuserViewModel, FragmentSuperuserBinding>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.fragment_superuser
|
||||||
|
override val viewModel: SuperuserViewModel by viewModel()
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
requireActivity().setTitle(R.string.superuser)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
viewModel.updatePolicies()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.superuser
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.res.Resources
|
||||||
|
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||||
|
import com.skoumal.teanity.extensions.applySchedulers
|
||||||
|
import com.skoumal.teanity.extensions.subscribeK
|
||||||
|
import com.skoumal.teanity.rxbus.RxBus
|
||||||
|
import com.skoumal.teanity.util.DiffObservableList
|
||||||
|
import com.skoumal.teanity.viewevents.SnackbarEvent
|
||||||
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.data.database.MagiskDB
|
||||||
|
import com.topjohnwu.magisk.model.entity.Policy
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
|
||||||
|
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||||
|
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||||
|
import com.topjohnwu.magisk.utils.toggle
|
||||||
|
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
|
||||||
|
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
|
||||||
|
import io.reactivex.Single
|
||||||
|
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||||
|
|
||||||
|
class SuperuserViewModel(
|
||||||
|
private val database: MagiskDB,
|
||||||
|
private val packageManager: PackageManager,
|
||||||
|
private val resources: Resources,
|
||||||
|
rxBus: RxBus
|
||||||
|
) : MagiskViewModel() {
|
||||||
|
|
||||||
|
val items = DiffObservableList(ComparableRvItem.callback)
|
||||||
|
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item ->
|
||||||
|
item.bind(itemBinding)
|
||||||
|
itemBinding.bindExtra(BR.viewModel, this@SuperuserViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var ignoreNext: PolicyRvItem? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
rxBus.register<PolicyEnableEvent>()
|
||||||
|
.subscribeK { togglePolicy(it.item, it.enable) }
|
||||||
|
.add()
|
||||||
|
rxBus.register<PolicyUpdateEvent>()
|
||||||
|
.subscribeK { updatePolicy(it) }
|
||||||
|
.add()
|
||||||
|
|
||||||
|
updatePolicies()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePolicies() {
|
||||||
|
Single.fromCallable { database.policyList }
|
||||||
|
.flattenAsFlowable { it }
|
||||||
|
.map { PolicyRvItem(it, it.info.loadIcon(packageManager)) }
|
||||||
|
.toList()
|
||||||
|
.applySchedulers()
|
||||||
|
.applyViewModel(this)
|
||||||
|
.subscribeK { items.update(it) }
|
||||||
|
.add()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deletePressed(item: PolicyRvItem) {
|
||||||
|
fun updateState() = deletePolicy(item.item)
|
||||||
|
.map { items.filterIsInstance<PolicyRvItem>().toMutableList() }
|
||||||
|
.map { it.removeAll { it.item.packageName == item.item.packageName }; it }
|
||||||
|
.map { it to items.calculateDiff(it) }
|
||||||
|
.subscribeK { items.update(it.first, it.second) }
|
||||||
|
.add()
|
||||||
|
|
||||||
|
withView {
|
||||||
|
if (FingerprintHelper.useFingerprint()) {
|
||||||
|
FingerprintAuthDialog(this) { updateState() }.show()
|
||||||
|
} else {
|
||||||
|
CustomAlertDialog(this)
|
||||||
|
.setTitle(R.string.su_revoke_title)
|
||||||
|
.setMessage(getString(R.string.su_revoke_msg, item.item.appName))
|
||||||
|
.setPositiveButton(R.string.yes) { _, _ -> updateState() }
|
||||||
|
.setNegativeButton(R.string.no_thanks, null)
|
||||||
|
.setCancelable(true)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePolicy(it: PolicyUpdateEvent) = when (it) {
|
||||||
|
is PolicyUpdateEvent.Notification -> updatePolicy(it.item) {
|
||||||
|
val textId = if (it.logging) R.string.su_snack_notif_on else R.string.su_snack_notif_off
|
||||||
|
val text = resources.getString(textId).format(it.appName)
|
||||||
|
SnackbarEvent(text).publish()
|
||||||
|
}
|
||||||
|
is PolicyUpdateEvent.Log -> updatePolicy(it.item) {
|
||||||
|
val textId =
|
||||||
|
if (it.notification) R.string.su_snack_log_on else R.string.su_snack_log_off
|
||||||
|
val text = resources.getString(textId).format(it.appName)
|
||||||
|
SnackbarEvent(text).publish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePolicy(item: PolicyRvItem, onSuccess: (Policy) -> Unit) =
|
||||||
|
updatePolicy(item.item)
|
||||||
|
.subscribeK { onSuccess(it) }
|
||||||
|
.add()
|
||||||
|
|
||||||
|
private fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
|
||||||
|
fun updateState() {
|
||||||
|
item.item.policy = if (enable) Policy.ALLOW else Policy.DENY
|
||||||
|
|
||||||
|
updatePolicy(item.item)
|
||||||
|
.map { it.policy == Policy.ALLOW }
|
||||||
|
.subscribeK {
|
||||||
|
val textId = if (it) R.string.su_snack_grant else R.string.su_snack_deny
|
||||||
|
val text = resources.getString(textId).format(item.item.appName)
|
||||||
|
SnackbarEvent(text).publish()
|
||||||
|
}
|
||||||
|
.add()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FingerprintHelper.useFingerprint()) {
|
||||||
|
withView {
|
||||||
|
FingerprintAuthDialog(this, { updateState() }, {
|
||||||
|
ignoreNext = item
|
||||||
|
item.isEnabled.toggle()
|
||||||
|
}).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePolicy(policy: Policy) =
|
||||||
|
Single.fromCallable { database.updatePolicy(policy); policy }
|
||||||
|
.applySchedulers()
|
||||||
|
|
||||||
|
private fun deletePolicy(policy: Policy) =
|
||||||
|
Single.fromCallable { database.deletePolicy(policy); policy }
|
||||||
|
.applySchedulers()
|
||||||
|
|
||||||
|
}
|
@ -1,287 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.surequest;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.hardware.fingerprint.FingerprintManager;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.CountDownTimer;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.App;
|
|
||||||
import com.topjohnwu.magisk.BuildConfig;
|
|
||||||
import com.topjohnwu.magisk.Config;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.model.entity.Policy;
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseActivity;
|
|
||||||
import com.topjohnwu.magisk.utils.FingerprintHelper;
|
|
||||||
import com.topjohnwu.magisk.utils.SuConnector;
|
|
||||||
import com.topjohnwu.magisk.utils.SuLogger;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import java9.lang.Iterables;
|
|
||||||
|
|
||||||
public class SuRequestActivity extends BaseActivity {
|
|
||||||
|
|
||||||
@BindView(R.id.su_popup) LinearLayout suPopup;
|
|
||||||
@BindView(R.id.timeout) Spinner timeout;
|
|
||||||
@BindView(R.id.app_icon) ImageView appIcon;
|
|
||||||
@BindView(R.id.app_name) TextView appNameView;
|
|
||||||
@BindView(R.id.package_name) TextView packageNameView;
|
|
||||||
@BindView(R.id.grant_btn) Button grant_btn;
|
|
||||||
@BindView(R.id.deny_btn) Button deny_btn;
|
|
||||||
@BindView(R.id.fingerprint) ImageView fingerprintImg;
|
|
||||||
@BindView(R.id.warning) TextView warning;
|
|
||||||
|
|
||||||
private ActionHandler handler;
|
|
||||||
private Policy policy;
|
|
||||||
private SharedPreferences timeoutPrefs;
|
|
||||||
|
|
||||||
public static final String REQUEST = "request";
|
|
||||||
public static final String LOG = "log";
|
|
||||||
public static final String NOTIFY = "notify";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getDarkTheme() {
|
|
||||||
return R.style.SuRequest_Dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
handler.handleAction(Policy.DENY, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
lockOrientation();
|
|
||||||
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
|
|
||||||
|
|
||||||
timeoutPrefs = App.deContext.getSharedPreferences("su_timeout", 0);
|
|
||||||
Intent intent = getIntent();
|
|
||||||
|
|
||||||
String action = intent.getAction();
|
|
||||||
|
|
||||||
if (TextUtils.equals(action, REQUEST)) {
|
|
||||||
if (!handleRequest())
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TextUtils.equals(action, LOG))
|
|
||||||
SuLogger.handleLogs(intent);
|
|
||||||
else if (TextUtils.equals(action, NOTIFY))
|
|
||||||
SuLogger.handleNotify(intent);
|
|
||||||
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean handleRequest() {
|
|
||||||
String socketName = getIntent().getStringExtra("socket");
|
|
||||||
|
|
||||||
if (socketName == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
SuConnector connector;
|
|
||||||
try {
|
|
||||||
connector = new SuConnector(socketName) {
|
|
||||||
@Override
|
|
||||||
protected void onResponse() throws IOException {
|
|
||||||
out.writeInt(policy.policy);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Bundle bundle = connector.readSocketInput();
|
|
||||||
int uid = Integer.parseInt(bundle.getString("uid"));
|
|
||||||
app.mDB.clearOutdated();
|
|
||||||
policy = app.mDB.getPolicy(uid);
|
|
||||||
if (policy == null) {
|
|
||||||
policy = new Policy(uid, getPackageManager());
|
|
||||||
}
|
|
||||||
} catch (IOException | PackageManager.NameNotFoundException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
handler = new ActionHandler() {
|
|
||||||
@Override
|
|
||||||
void handleAction() {
|
|
||||||
connector.response();
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void handleAction(int action) {
|
|
||||||
int pos = timeout.getSelectedItemPosition();
|
|
||||||
timeoutPrefs.edit().putInt(policy.packageName, pos).apply();
|
|
||||||
handleAction(action, Config.Value.TIMEOUT_LIST[pos]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void handleAction(int action, int time) {
|
|
||||||
policy.policy = action;
|
|
||||||
if (time >= 0) {
|
|
||||||
policy.until = (time == 0) ? 0
|
|
||||||
: (System.currentTimeMillis() / 1000 + time * 60);
|
|
||||||
app.mDB.updatePolicy(policy);
|
|
||||||
}
|
|
||||||
handleAction();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Never allow com.topjohnwu.magisk (could be malware)
|
|
||||||
if (TextUtils.equals(policy.packageName, BuildConfig.APPLICATION_ID))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// If not interactive, response directly
|
|
||||||
if (policy.policy != Policy.INTERACTIVE) {
|
|
||||||
handler.handleAction();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ((int) Config.get(Config.Key.SU_AUTO_RESPONSE)) {
|
|
||||||
case Config.Value.SU_AUTO_DENY:
|
|
||||||
handler.handleAction(Policy.DENY, 0);
|
|
||||||
return true;
|
|
||||||
case Config.Value.SU_AUTO_ALLOW:
|
|
||||||
handler.handleAction(Policy.ALLOW, 0);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
showUI();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
private void showUI() {
|
|
||||||
setContentView(R.layout.activity_request);
|
|
||||||
new SuRequestActivity_ViewBinding(this);
|
|
||||||
|
|
||||||
appIcon.setImageDrawable(policy.info.loadIcon(getPackageManager()));
|
|
||||||
appNameView.setText(policy.appName);
|
|
||||||
packageNameView.setText(policy.packageName);
|
|
||||||
warning.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
|
||||||
AppCompatResources.getDrawable(this, R.drawable.ic_warning), null, null, null);
|
|
||||||
|
|
||||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
|
|
||||||
R.array.allow_timeout, android.R.layout.simple_spinner_item);
|
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
||||||
timeout.setAdapter(adapter);
|
|
||||||
timeout.setSelection(timeoutPrefs.getInt(policy.packageName, 0));
|
|
||||||
|
|
||||||
CountDownTimer timer = new CountDownTimer(
|
|
||||||
(int) Config.get(Config.Key.SU_REQUEST_TIMEOUT) * 1000, 1000) {
|
|
||||||
@Override
|
|
||||||
public void onTick(long remains) {
|
|
||||||
deny_btn.setText(getString(R.string.deny) + "(" + remains / 1000 + ")");
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void onFinish() {
|
|
||||||
deny_btn.setText(getString(R.string.deny));
|
|
||||||
handler.handleAction(Policy.DENY);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
timer.start();
|
|
||||||
Runnable cancelTimer = () -> {
|
|
||||||
timer.cancel();
|
|
||||||
deny_btn.setText(getString(R.string.deny));
|
|
||||||
};
|
|
||||||
handler.addCancel(cancelTimer);
|
|
||||||
|
|
||||||
boolean useFP = FingerprintHelper.useFingerprint();
|
|
||||||
|
|
||||||
if (useFP) try {
|
|
||||||
FingerprintHelper helper = new SuFingerprint();
|
|
||||||
helper.authenticate();
|
|
||||||
handler.addCancel(helper::cancel);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
useFP = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!useFP) {
|
|
||||||
grant_btn.setOnClickListener(v -> {
|
|
||||||
handler.handleAction(Policy.ALLOW);
|
|
||||||
timer.cancel();
|
|
||||||
});
|
|
||||||
grant_btn.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
grant_btn.setVisibility(useFP ? View.GONE : View.VISIBLE);
|
|
||||||
fingerprintImg.setVisibility(useFP ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
deny_btn.setOnClickListener(v -> {
|
|
||||||
handler.handleAction(Policy.DENY);
|
|
||||||
timer.cancel();
|
|
||||||
});
|
|
||||||
suPopup.setOnClickListener(v -> cancelTimer.run());
|
|
||||||
timeout.setOnTouchListener((v, event) -> {
|
|
||||||
cancelTimer.run();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SuFingerprint extends FingerprintHelper {
|
|
||||||
|
|
||||||
SuFingerprint() throws Exception {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationError(int errorCode, CharSequence errString) {
|
|
||||||
warning.setText(errString);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
|
|
||||||
warning.setText(helpString);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
|
|
||||||
handler.handleAction(Policy.ALLOW);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationFailed() {
|
|
||||||
warning.setText(R.string.auth_fail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ActionHandler {
|
|
||||||
private List<Runnable> cancelTasks = new ArrayList<>();
|
|
||||||
|
|
||||||
void handleAction() {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleAction(int action) {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleAction(int action, int time) {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
void addCancel(Runnable r) {
|
|
||||||
cancelTasks.add(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
void done() {
|
|
||||||
Iterables.forEach(cancelTasks, Runnable::run);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,67 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.surequest
|
||||||
|
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.Window
|
||||||
|
import com.skoumal.teanity.viewevents.ViewEvent
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.databinding.ActivityRequestBinding
|
||||||
|
import com.topjohnwu.magisk.model.entity.Policy
|
||||||
|
import com.topjohnwu.magisk.model.events.DieEvent
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskActivity
|
||||||
|
import com.topjohnwu.magisk.utils.SuLogger
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
|
open class SuRequestActivity : MagiskActivity<SuRequestViewModel, ActivityRequestBinding>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.activity_request
|
||||||
|
override val viewModel: SuRequestViewModel by viewModel()
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
viewModel.handler?.handleAction(Policy.DENY, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
|
lockOrientation()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val intent = intent
|
||||||
|
val action = intent.action
|
||||||
|
|
||||||
|
if (TextUtils.equals(action, REQUEST)) {
|
||||||
|
if (!viewModel.handleRequest(intent) {})
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TextUtils.equals(action, LOG))
|
||||||
|
SuLogger.handleLogs(intent)
|
||||||
|
else if (TextUtils.equals(action, NOTIFY))
|
||||||
|
SuLogger.handleNotify(intent)
|
||||||
|
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEventDispatched(event: ViewEvent) {
|
||||||
|
super.onEventDispatched(event)
|
||||||
|
when (event) {
|
||||||
|
is DieEvent -> finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun lockOrientation() {
|
||||||
|
requestedOrientation = if (Build.VERSION.SDK_INT < 18)
|
||||||
|
resources.configuration.orientation
|
||||||
|
else
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val REQUEST = "request"
|
||||||
|
const val LOG = "log"
|
||||||
|
const val NOTIFY = "notify"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,251 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.surequest
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
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.hardware.fingerprint.FingerprintManager
|
||||||
|
import android.os.CountDownTimer
|
||||||
|
import android.text.TextUtils
|
||||||
|
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||||
|
import com.skoumal.teanity.util.DiffObservableList
|
||||||
|
import com.skoumal.teanity.util.KObservableField
|
||||||
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
import com.topjohnwu.magisk.Config
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.data.database.MagiskDB
|
||||||
|
import com.topjohnwu.magisk.model.entity.Policy
|
||||||
|
import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem
|
||||||
|
import com.topjohnwu.magisk.model.events.DieEvent
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||||
|
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||||
|
import com.topjohnwu.magisk.utils.SuConnector
|
||||||
|
import com.topjohnwu.magisk.utils.now
|
||||||
|
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.TimeUnit.*
|
||||||
|
|
||||||
|
class SuRequestViewModel(
|
||||||
|
private val packageManager: PackageManager,
|
||||||
|
private val database: MagiskDB,
|
||||||
|
private val timeoutPrefs: SharedPreferences,
|
||||||
|
private val resources: Resources
|
||||||
|
) : MagiskViewModel() {
|
||||||
|
|
||||||
|
val icon = KObservableField<Drawable?>(null)
|
||||||
|
val title = KObservableField("")
|
||||||
|
val packageName = KObservableField("")
|
||||||
|
|
||||||
|
val denyText = KObservableField(resources.getString(R.string.deny))
|
||||||
|
val warningText = KObservableField<CharSequence>(resources.getString(R.string.su_warning))
|
||||||
|
|
||||||
|
val canUseFingerprint = KObservableField(FingerprintHelper.useFingerprint())
|
||||||
|
val selectedItemPosition = KObservableField(0)
|
||||||
|
|
||||||
|
val items = DiffObservableList(ComparableRvItem.callback)
|
||||||
|
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { binding, _, item ->
|
||||||
|
item.bind(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var handler: ActionHandler? = null
|
||||||
|
private var timer: CountDownTimer? = null
|
||||||
|
private var policy: Policy? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
updatePolicy(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
resources.getStringArray(R.array.allow_timeout)
|
||||||
|
.map { SpinnerRvItem(it) }
|
||||||
|
.let { items.update(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePolicy(policy: Policy?) {
|
||||||
|
policy ?: return
|
||||||
|
|
||||||
|
icon.value = policy.info.loadIcon(packageManager)
|
||||||
|
title.value = policy.appName
|
||||||
|
packageName.value = policy.packageName
|
||||||
|
|
||||||
|
selectedItemPosition.value = timeoutPrefs.getInt(policy.packageName, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelTimer() {
|
||||||
|
timer?.cancel()
|
||||||
|
denyText.value = resources.getString(R.string.deny)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun grantPressed() {
|
||||||
|
handler?.handleAction(Policy.ALLOW)
|
||||||
|
timer?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun denyPressed() {
|
||||||
|
handler?.handleAction(Policy.DENY)
|
||||||
|
timer?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun spinnerTouched(): Boolean {
|
||||||
|
cancelTimer()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleRequest(intent: Intent, createUICallback: () -> Unit): Boolean {
|
||||||
|
val socketName = intent.getStringExtra("socket") ?: return false
|
||||||
|
|
||||||
|
val connector: SuConnector
|
||||||
|
try {
|
||||||
|
connector = object : SuConnector(socketName) {
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun onResponse() {
|
||||||
|
out.writeInt(policy?.policy ?: return)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val bundle = connector.readSocketInput()
|
||||||
|
val uid = bundle.getString("uid")?.toIntOrNull() ?: return false
|
||||||
|
database.clearOutdated()
|
||||||
|
policy = database.getPolicy(uid) ?: Policy(uid, packageManager)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
handler = object : ActionHandler() {
|
||||||
|
override fun handleAction() {
|
||||||
|
connector.response()
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleAction(action: Int) {
|
||||||
|
val pos = selectedItemPosition.value
|
||||||
|
timeoutPrefs.edit().putInt(policy?.packageName, pos).apply()
|
||||||
|
handleAction(action, Config.Value.TIMEOUT_LIST[pos])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleAction(action: Int, time: Int) {
|
||||||
|
policy?.apply {
|
||||||
|
policy = action
|
||||||
|
if (time >= 0) {
|
||||||
|
until = if (time == 0) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
MILLISECONDS.toSeconds(now) + MINUTES.toSeconds(time.toLong())
|
||||||
|
}
|
||||||
|
database.updatePolicy(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
policy?.policy = action
|
||||||
|
|
||||||
|
handleAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never allow com.topjohnwu.magisk (could be malware)
|
||||||
|
if (TextUtils.equals(policy?.packageName, BuildConfig.APPLICATION_ID))
|
||||||
|
return false
|
||||||
|
|
||||||
|
// If not interactive, response directly
|
||||||
|
if (policy?.policy != Policy.INTERACTIVE) {
|
||||||
|
handler?.handleAction()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
when (Config.get<Any>(Config.Key.SU_AUTO_RESPONSE) as Int) {
|
||||||
|
Config.Value.SU_AUTO_DENY -> {
|
||||||
|
handler?.handleAction(Policy.DENY, 0)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
Config.Value.SU_AUTO_ALLOW -> {
|
||||||
|
handler?.handleAction(Policy.ALLOW, 0)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createUICallback()
|
||||||
|
showUI()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
private fun showUI() {
|
||||||
|
val seconds = Config.get<Int>(Config.Key.SU_REQUEST_TIMEOUT).toLong()
|
||||||
|
val millis = SECONDS.toMillis(seconds)
|
||||||
|
timer = object : CountDownTimer(millis, 1000) {
|
||||||
|
override fun onTick(remains: Long) {
|
||||||
|
denyText.value = "%s (%d)"
|
||||||
|
.format(resources.getString(R.string.deny), remains / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinish() {
|
||||||
|
denyText.value = resources.getString(R.string.deny)
|
||||||
|
handler?.handleAction(Policy.DENY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timer?.start()
|
||||||
|
handler?.addCancel(Runnable { cancelTimer() })
|
||||||
|
|
||||||
|
val useFP = canUseFingerprint.value
|
||||||
|
|
||||||
|
if (useFP)
|
||||||
|
try {
|
||||||
|
val helper = SuFingerprint()
|
||||||
|
helper.authenticate()
|
||||||
|
handler?.addCancel(Runnable { helper.cancel() })
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SuFingerprint @Throws(Exception::class)
|
||||||
|
internal constructor() : FingerprintHelper() {
|
||||||
|
|
||||||
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
|
warningText.value = errString
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence) {
|
||||||
|
warningText.value = helpString
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
|
||||||
|
handler?.handleAction(Policy.ALLOW)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationFailed() {
|
||||||
|
warningText.value = resources.getString(R.string.auth_fail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open inner class ActionHandler {
|
||||||
|
private val cancelTasks = mutableListOf<Runnable>()
|
||||||
|
|
||||||
|
internal open fun handleAction() {
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal open fun handleAction(action: Int) {
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal open fun handleAction(action: Int, time: Int) {
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun addCancel(r: Runnable) {
|
||||||
|
cancelTasks.add(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun done() {
|
||||||
|
cancelTasks.forEach { it.run() }
|
||||||
|
DieEvent().publish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,166 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.surequest
|
||||||
|
|
||||||
|
import android.hardware.fingerprint.FingerprintManager
|
||||||
|
import android.os.CountDownTimer
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.text.bold
|
||||||
|
import com.skoumal.teanity.viewevents.ViewEvent
|
||||||
|
import com.topjohnwu.magisk.Config
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.databinding.ActivitySuRequestBinding
|
||||||
|
import com.topjohnwu.magisk.model.entity.Policy
|
||||||
|
import com.topjohnwu.magisk.model.events.DieEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.SuDialogEvent
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskActivity
|
||||||
|
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||||
|
import com.topjohnwu.magisk.utils.feature.WIP
|
||||||
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||||
|
import java.util.concurrent.TimeUnit.SECONDS
|
||||||
|
|
||||||
|
@WIP
|
||||||
|
open class _SuRequestActivity : MagiskActivity<_SuRequestViewModel, ActivitySuRequestBinding>() {
|
||||||
|
|
||||||
|
override val layoutRes: Int = R.layout.activity_su_request
|
||||||
|
override val viewModel: _SuRequestViewModel by viewModel {
|
||||||
|
parametersOf(intent, intent.action)
|
||||||
|
}
|
||||||
|
|
||||||
|
//private val timeoutPrefs: SharedPreferences by inject(SUTimeout)
|
||||||
|
private val canUseFingerprint get() = FingerprintHelper.useFingerprint()
|
||||||
|
|
||||||
|
private val countdown by lazy {
|
||||||
|
val seconds = Config.get<Int>(Config.Key.SU_REQUEST_TIMEOUT).toLong()
|
||||||
|
val millis = SECONDS.toMillis(seconds)
|
||||||
|
object : CountDownTimer(millis, 1000) {
|
||||||
|
override fun onFinish() {
|
||||||
|
viewModel.deny()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
|
dialog.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
|
Timber.e("Tick, tock")
|
||||||
|
title = "%s (%d)".format(
|
||||||
|
getString(R.string.deny),
|
||||||
|
MILLISECONDS.toSeconds(millisUntilFinished)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var fingerprintHelper: SuFingerprint? = null
|
||||||
|
|
||||||
|
private lateinit var dialog: MagiskDialog
|
||||||
|
|
||||||
|
override fun onEventDispatched(event: ViewEvent) {
|
||||||
|
super.onEventDispatched(event)
|
||||||
|
when (event) {
|
||||||
|
is SuDialogEvent -> showDialog(event.policy)
|
||||||
|
is DieEvent -> finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (::dialog.isInitialized && dialog.isShowing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
if (this::dialog.isInitialized && dialog.isShowing) {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
fingerprintHelper?.cancel()
|
||||||
|
countdown.cancel()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDialog(policy: Policy) {
|
||||||
|
val titleText = SpannableStringBuilder("Allow ")
|
||||||
|
.bold { append(policy.appName) }
|
||||||
|
.append(" to access superuser rights?")
|
||||||
|
|
||||||
|
val messageText = StringBuilder()
|
||||||
|
.appendln(policy.packageName)
|
||||||
|
.append(getString(R.string.su_warning))
|
||||||
|
|
||||||
|
dialog = MagiskDialog(this)
|
||||||
|
.applyIcon(policy.info.loadIcon(packageManager))
|
||||||
|
.applyTitle(titleText)
|
||||||
|
.applyMessage(messageText)
|
||||||
|
//.applyView()) {} //todo add a spinner
|
||||||
|
.cancellable(false)
|
||||||
|
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
|
titleRes = R.string.grant
|
||||||
|
onClick { viewModel.grant() }
|
||||||
|
if (canUseFingerprint) {
|
||||||
|
icon = R.drawable.ic_fingerprint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.applyButton(MagiskDialog.ButtonType.NEUTRAL) {
|
||||||
|
title = "%s %s".format(getString(R.string.grant), getString(R.string.once))
|
||||||
|
onClick { viewModel.grant(-1) }
|
||||||
|
}
|
||||||
|
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
|
titleRes = R.string.deny
|
||||||
|
onClick { viewModel.deny() }
|
||||||
|
}
|
||||||
|
.onDismiss { finish() }
|
||||||
|
.onShow {
|
||||||
|
startTimer().also { Timber.e("Starting timer") }
|
||||||
|
if (canUseFingerprint) {
|
||||||
|
startFingerprintQuery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.reveal()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startTimer() {
|
||||||
|
countdown.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startFingerprintQuery() {
|
||||||
|
val result = runCatching {
|
||||||
|
fingerprintHelper = SuFingerprint().apply { authenticate() }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
dialog.applyButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
|
icon = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SuFingerprint @Throws(Exception::class)
|
||||||
|
internal constructor() : FingerprintHelper() {
|
||||||
|
|
||||||
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
|
Toast.makeText(this@_SuRequestActivity, errString, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence) {
|
||||||
|
Toast.makeText(this@_SuRequestActivity, helpString, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
|
||||||
|
viewModel.grant()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationFailed() {
|
||||||
|
Toast.makeText(this@_SuRequestActivity, R.string.auth_fail, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val REQUEST = "request"
|
||||||
|
const val LOG = "log"
|
||||||
|
const val NOTIFY = "notify"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,112 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.surequest
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import com.skoumal.teanity.extensions.subscribeK
|
||||||
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
import com.topjohnwu.magisk.Config
|
||||||
|
import com.topjohnwu.magisk.data.database.MagiskDB
|
||||||
|
import com.topjohnwu.magisk.model.entity.Policy
|
||||||
|
import com.topjohnwu.magisk.model.events.DieEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.SuDialogEvent
|
||||||
|
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||||
|
import com.topjohnwu.magisk.utils.SuConnector
|
||||||
|
import com.topjohnwu.magisk.utils.SuLogger
|
||||||
|
import com.topjohnwu.magisk.utils.feature.WIP
|
||||||
|
import com.topjohnwu.magisk.utils.now
|
||||||
|
import io.reactivex.Single
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||||
|
import java.util.concurrent.TimeUnit.MINUTES
|
||||||
|
|
||||||
|
@WIP
|
||||||
|
class _SuRequestViewModel(
|
||||||
|
intent: Intent,
|
||||||
|
action: String,
|
||||||
|
private val packageManager: PackageManager,
|
||||||
|
private val database: MagiskDB
|
||||||
|
) : MagiskViewModel() {
|
||||||
|
|
||||||
|
private val connector: Single<SuConnector> = Single.fromCallable {
|
||||||
|
val socketName = intent.extras?.getString("socket") ?: let {
|
||||||
|
deny()
|
||||||
|
throw IllegalStateException("Socket is empty or null")
|
||||||
|
}
|
||||||
|
object : SuConnector(socketName) {
|
||||||
|
override fun onResponse() {
|
||||||
|
policy.subscribeK { out.writeInt(it.policy) } //this just might be incorrect, lol
|
||||||
|
}
|
||||||
|
} as SuConnector
|
||||||
|
}.cache()
|
||||||
|
|
||||||
|
private val policy: Single<Policy> = connector.map {
|
||||||
|
val bundle = it.readSocketInput() ?: throw IllegalStateException("Socket bundle is null")
|
||||||
|
val uid = bundle.getString("uid")?.toIntOrNull() ?: let {
|
||||||
|
deny()
|
||||||
|
throw IllegalStateException("UID is empty or null")
|
||||||
|
}
|
||||||
|
database.clearOutdated()
|
||||||
|
database.getPolicy(uid) ?: Policy(uid, packageManager)
|
||||||
|
}.cache()
|
||||||
|
|
||||||
|
init {
|
||||||
|
when (action) {
|
||||||
|
SuRequestActivity.LOG -> SuLogger.handleLogs(intent).also { die() }
|
||||||
|
SuRequestActivity.NOTIFY -> SuLogger.handleNotify(intent).also { die() }
|
||||||
|
SuRequestActivity.REQUEST -> process()
|
||||||
|
else -> back() // invalid action, should ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun process() {
|
||||||
|
policy.subscribeK(onError = ::deny) { process(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun process(policy: Policy) {
|
||||||
|
if (policy.packageName == BuildConfig.APPLICATION_ID)
|
||||||
|
deny().also { return }
|
||||||
|
|
||||||
|
if (policy.policy != Policy.INTERACTIVE)
|
||||||
|
grant().also { return }
|
||||||
|
|
||||||
|
when (Config.get<Int>(Config.Key.SU_AUTO_RESPONSE)) {
|
||||||
|
Config.Value.SU_AUTO_DENY -> deny().also { return }
|
||||||
|
Config.Value.SU_AUTO_ALLOW -> grant().also { return }
|
||||||
|
}
|
||||||
|
|
||||||
|
requestDialog(policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deny(e: Throwable? = null) = updatePolicy(Policy.DENY, 0).also { Timber.e(e) }
|
||||||
|
fun grant(time: Long = 0) = updatePolicy(Policy.ALLOW, time)
|
||||||
|
|
||||||
|
private fun updatePolicy(action: Int, time: Long) {
|
||||||
|
|
||||||
|
fun finish(e: Throwable? = null) = die().also { Timber.e(e) }
|
||||||
|
|
||||||
|
policy
|
||||||
|
.map { it.policy = action; it }
|
||||||
|
.doOnSuccess {
|
||||||
|
if (time >= 0) {
|
||||||
|
it.until = if (time == 0L) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
MILLISECONDS.toSeconds(now) + MINUTES.toSeconds(time)
|
||||||
|
}
|
||||||
|
database.updatePolicy(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatMap { connector }
|
||||||
|
.subscribeK(onError = ::finish) {
|
||||||
|
it.response()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestDialog(policy: Policy) {
|
||||||
|
SuDialogEvent(policy).publish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun die() = DieEvent().publish()
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,144 @@
|
|||||||
|
package com.topjohnwu.magisk.utils
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.databinding.BindingAdapter
|
||||||
|
import androidx.databinding.InverseBindingAdapter
|
||||||
|
import androidx.databinding.InverseBindingListener
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
|
import androidx.viewpager.widget.ViewPager
|
||||||
|
import com.google.android.material.navigation.NavigationView
|
||||||
|
import com.skoumal.teanity.extensions.subscribeK
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.disposables.Disposable
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
|
||||||
|
@BindingAdapter("onNavigationClick")
|
||||||
|
fun setOnNavigationClickedListener(view: Toolbar, listener: View.OnClickListener) {
|
||||||
|
view.setNavigationOnClickListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("onNavigationClick")
|
||||||
|
fun setOnNavigationClickedListener(
|
||||||
|
view: NavigationView,
|
||||||
|
listener: NavigationView.OnNavigationItemSelectedListener
|
||||||
|
) {
|
||||||
|
view.setNavigationItemSelectedListener {
|
||||||
|
(view.parent as? DrawerLayout)?.closeDrawers()
|
||||||
|
listener.onNavigationItemSelected(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("srcCompat")
|
||||||
|
fun setImageResource(view: AppCompatImageView, @DrawableRes resId: Int) {
|
||||||
|
view.setImageResource(resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("app:tint")
|
||||||
|
fun setTint(view: AppCompatImageView, @ColorInt tint: Int) {
|
||||||
|
view.setColorFilter(tint)
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("isChecked")
|
||||||
|
fun setChecked(view: AppCompatImageView, isChecked: Boolean) {
|
||||||
|
val state = when (isChecked) {
|
||||||
|
true -> IndeterminateState.CHECKED
|
||||||
|
else -> IndeterminateState.UNCHECKED
|
||||||
|
}
|
||||||
|
setChecked(view, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("isChecked")
|
||||||
|
fun setChecked(view: AppCompatImageView, isChecked: IndeterminateState) {
|
||||||
|
view.setImageResource(
|
||||||
|
when (isChecked) {
|
||||||
|
IndeterminateState.INDETERMINATE -> R.drawable.ic_indeterminate
|
||||||
|
IndeterminateState.CHECKED -> R.drawable.ic_checked
|
||||||
|
IndeterminateState.UNCHECKED -> R.drawable.ic_unchecked
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("position")
|
||||||
|
fun setPosition(view: ViewPager, position: Int) {
|
||||||
|
view.currentItem = position
|
||||||
|
}
|
||||||
|
|
||||||
|
@InverseBindingAdapter(attribute = "position", event = "positionChanged")
|
||||||
|
fun getPosition(view: ViewPager) = view.currentItem
|
||||||
|
|
||||||
|
@BindingAdapter("positionChanged")
|
||||||
|
fun setPositionChangedListener(view: ViewPager, listener: InverseBindingListener) {
|
||||||
|
view.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
|
||||||
|
override fun onPageSelected(position: Int) = listener.onChange()
|
||||||
|
override fun onPageScrollStateChanged(state: Int) = listener.onChange()
|
||||||
|
override fun onPageScrolled(
|
||||||
|
position: Int,
|
||||||
|
positionOffset: Float,
|
||||||
|
positionOffsetPixels: Int
|
||||||
|
) = listener.onChange()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("invisibleScale")
|
||||||
|
fun setInvisibleWithScale(view: View, isInvisible: Boolean) {
|
||||||
|
view.animate()
|
||||||
|
.scaleX(if (isInvisible) 0f else 1f)
|
||||||
|
.scaleY(if (isInvisible) 0f else 1f)
|
||||||
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("movieBehavior", "movieBehaviorText")
|
||||||
|
fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) {
|
||||||
|
(view.tag as? Disposable)?.dispose()
|
||||||
|
if (isMovieBehavior) {
|
||||||
|
val observer = Observable
|
||||||
|
.interval(150, TimeUnit.MILLISECONDS)
|
||||||
|
.subscribeK {
|
||||||
|
view.text = text.replaceRandomWithSpecial()
|
||||||
|
}
|
||||||
|
view.tag = observer
|
||||||
|
} else {
|
||||||
|
view.text = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("android:selectedItemPosition")
|
||||||
|
fun setSelectedItemPosition(view: Spinner, position: Int) {
|
||||||
|
view.setSelection(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
@InverseBindingAdapter(
|
||||||
|
attribute = "android:selectedItemPosition",
|
||||||
|
event = "android:selectedItemPositionAttrChanged"
|
||||||
|
)
|
||||||
|
fun getSelectedItemPosition(view: Spinner) = view.selectedItemPosition
|
||||||
|
|
||||||
|
@BindingAdapter("android:selectedItemPositionAttrChanged")
|
||||||
|
fun setSelectedItemPositionListener(view: Spinner, listener: InverseBindingListener) {
|
||||||
|
view.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onNothingSelected(p0: AdapterView<*>?) {
|
||||||
|
listener.onChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
|
||||||
|
listener.onChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("onTouch")
|
||||||
|
fun setOnTouchListener(view: View, listener: View.OnTouchListener) {
|
||||||
|
view.setOnTouchListener(listener)
|
||||||
|
}
|
@ -3,8 +3,6 @@ package com.topjohnwu.magisk.utils;
|
|||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.App;
|
import com.topjohnwu.magisk.App;
|
||||||
import com.topjohnwu.magisk.BuildConfig;
|
import com.topjohnwu.magisk.BuildConfig;
|
||||||
import com.topjohnwu.magisk.ClassMap;
|
import com.topjohnwu.magisk.ClassMap;
|
||||||
@ -29,6 +27,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.jar.JarEntry;
|
import java.util.jar.JarEntry;
|
||||||
|
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
public class PatchAPK {
|
public class PatchAPK {
|
||||||
|
|
||||||
public static final String LOWERALPHA = "abcdefghijklmnopqrstuvwxyz";
|
public static final String LOWERALPHA = "abcdefghijklmnopqrstuvwxyz";
|
||||||
|
@ -37,7 +37,7 @@ public class SuLogger {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Doesn't report whether notify or not, check database ourselves
|
// Doesn't report whether notify or not, check database ourselves
|
||||||
policy = app.mDB.getPolicy(fromUid);
|
policy = app.getDB().getPolicy(fromUid);
|
||||||
if (policy == null)
|
if (policy == null)
|
||||||
return;
|
return;
|
||||||
notify = policy.notification;
|
notify = policy.notification;
|
||||||
@ -62,7 +62,7 @@ public class SuLogger {
|
|||||||
log.fromPid = pid;
|
log.fromPid = pid;
|
||||||
log.command = command;
|
log.command = command;
|
||||||
log.date = new Date();
|
log.date = new Date();
|
||||||
app.mDB.addLog(log);
|
app.getDB().addLog(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void handleNotify(Policy policy) {
|
private static void handleNotify(Policy policy) {
|
||||||
|
@ -12,12 +12,6 @@ import android.net.Uri;
|
|||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.work.Constraints;
|
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy;
|
|
||||||
import androidx.work.NetworkType;
|
|
||||||
import androidx.work.PeriodicWorkRequest;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.App;
|
import com.topjohnwu.magisk.App;
|
||||||
import com.topjohnwu.magisk.BuildConfig;
|
import com.topjohnwu.magisk.BuildConfig;
|
||||||
import com.topjohnwu.magisk.ClassMap;
|
import com.topjohnwu.magisk.ClassMap;
|
||||||
@ -36,6 +30,12 @@ import java.util.Locale;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import androidx.work.Constraints;
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy;
|
||||||
|
import androidx.work.NetworkType;
|
||||||
|
import androidx.work.PeriodicWorkRequest;
|
||||||
|
import androidx.work.WorkManager;
|
||||||
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
|
|
||||||
public static void toast(CharSequence msg, int duration) {
|
public static void toast(CharSequence msg, int duration) {
|
||||||
|
50
app/src/main/java/com/topjohnwu/magisk/utils/XAndroid.kt
Normal file
50
app/src/main/java/com/topjohnwu/magisk/utils/XAndroid.kt
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package com.topjohnwu.magisk.utils
|
||||||
|
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.ComponentInfo
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.PackageManager.*
|
||||||
|
|
||||||
|
val PackageInfo.processes
|
||||||
|
get() = activities?.processNames.orEmpty() +
|
||||||
|
services?.processNames.orEmpty() +
|
||||||
|
receivers?.processNames.orEmpty() +
|
||||||
|
providers?.processNames.orEmpty()
|
||||||
|
|
||||||
|
val Array<out ComponentInfo>.processNames get() = mapNotNull { it.processName }
|
||||||
|
|
||||||
|
val ApplicationInfo.packageInfo: PackageInfo?
|
||||||
|
get() {
|
||||||
|
val pm: PackageManager by inject()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val request = GET_ACTIVITIES or
|
||||||
|
GET_SERVICES or
|
||||||
|
GET_RECEIVERS or
|
||||||
|
GET_PROVIDERS
|
||||||
|
pm.getPackageInfo(packageName, request)
|
||||||
|
} catch (e1: Exception) {
|
||||||
|
try {
|
||||||
|
pm.activities(packageName).apply {
|
||||||
|
services = pm.services(packageName)
|
||||||
|
receivers = pm.receivers(packageName)
|
||||||
|
providers = pm.providers(packageName)
|
||||||
|
}
|
||||||
|
} catch (e2: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PackageManager.activities(packageName: String) =
|
||||||
|
getPackageInfo(packageName, GET_ACTIVITIES)
|
||||||
|
|
||||||
|
fun PackageManager.services(packageName: String) =
|
||||||
|
getPackageInfo(packageName, GET_SERVICES).services
|
||||||
|
|
||||||
|
fun PackageManager.receivers(packageName: String) =
|
||||||
|
getPackageInfo(packageName, GET_RECEIVERS).receivers
|
||||||
|
|
||||||
|
fun PackageManager.providers(packageName: String) =
|
||||||
|
getPackageInfo(packageName, GET_PROVIDERS).providers
|
8
app/src/main/java/com/topjohnwu/magisk/utils/XBinding.kt
Normal file
8
app/src/main/java/com/topjohnwu/magisk/utils/XBinding.kt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package com.topjohnwu.magisk.utils
|
||||||
|
|
||||||
|
import com.skoumal.teanity.util.KObservableField
|
||||||
|
|
||||||
|
|
||||||
|
fun KObservableField<Boolean>.toggle() {
|
||||||
|
value = !value
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user