diff --git a/app/build.gradle b/app/build.gradle index a5b62dff3..dbc98c4fa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'com.android.application' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' kapt { @@ -16,6 +16,7 @@ android { defaultConfig { applicationId 'com.topjohnwu.magisk' vectorDrawables.useSupportLibrary = true + multiDexEnabled true versionName configProps['appVersion'] versionCode configProps['appVersionCode'] as Integer javaCompileOptions { @@ -30,6 +31,10 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + + dataBinding { + enabled = true + } } dependencies { @@ -37,25 +42,19 @@ dependencies { implementation project(':net') implementation project(':shared') implementation project(':signing') + implementation 'com.github.topjohnwu:jtar:1.0.0' implementation 'net.sourceforge.streamsupport:android-retrostreams:1.7.0' 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' implementation "ru.noties.markwon:core:${markwonVersion}" implementation "ru.noties.markwon:html:${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' implementation "com.github.topjohnwu.libsu:core:${libsuVersion}" implementation "com.github.topjohnwu.libsu:io:${libsuVersion}" @@ -63,4 +62,19 @@ dependencies { def butterKnifeVersion = '10.1.0' implementation "com.jakewharton:butterknife-runtime:${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' } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 00ff344e1..33f347808 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -29,6 +29,11 @@ void onResponse(int); } +# Keep all fragment constructors +-keepclassmembers class * extends androidx.fragment.app.Fragment { + public (...); +} + # DelegateWorker -keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index beb5505cd..4bf6ef98d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ @@ -35,7 +35,7 @@ android:name="a.f" android:configChanges="keyboardHidden|orientation|screenSize" android:screenOrientation="nosensor" - android:theme="@style/AppTheme.NoDrawer" /> + android:theme="@style/MagiskTheme.Flashing" /> @@ -44,7 +44,7 @@ android:exported="false" android:directBootAware="true" android:excludeFromRecents="true" - android:theme="@style/SuRequest" /> + android:theme="@style/MagiskTheme.SU" /> @@ -74,4 +74,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/com/topjohnwu/magisk/App.java b/app/src/main/java/com/topjohnwu/magisk/App.java deleted file mode 100644 index 301aa151e..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/App.java +++ /dev/null @@ -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) {} -} diff --git a/app/src/main/java/com/topjohnwu/magisk/App.kt b/app/src/main/java/com/topjohnwu/magisk/App.kt new file mode 100644 index 000000000..4fcec157c --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/App.kt @@ -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 + } + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/Config.java b/app/src/main/java/com/topjohnwu/magisk/Config.java index 10f4119ea..76f4580ba 100644 --- a/app/src/main/java/com/topjohnwu/magisk/Config.java +++ b/app/src/main/java/com/topjohnwu/magisk/Config.java @@ -3,8 +3,6 @@ package com.topjohnwu.magisk; import android.content.SharedPreferences; import android.util.Xml; -import androidx.collection.ArrayMap; - import com.topjohnwu.magisk.utils.Utils; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.ShellUtils; @@ -17,6 +15,8 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.IOException; +import androidx.collection.ArrayMap; + public class Config { // Current status @@ -109,14 +109,14 @@ public class Config { public static void export() { // Flush prefs to disk App app = App.self; - app.prefs.edit().commit(); + app.getPrefs().edit().commit(); File xml = new File(App.deContext.getFilesDir().getParent() + "/shared_prefs", app.getPackageName() + "_preferences.xml"); Shell.su(Utils.fmt("cat %s > /data/adb/%s", xml, Const.MANAGER_CONFIGS)).exec(); } public static void initialize() { - SharedPreferences pref = App.self.prefs; + SharedPreferences pref = App.self.getPrefs(); SharedPreferences.Editor editor = pref.edit(); File config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS); if (config.exists()) { @@ -238,19 +238,19 @@ public class Config { App app = App.self; switch (getConfigType(key)) { 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: - return (T) (Integer) Utils.getPrefsInt(app.prefs, key, getDef(key)); + return (T) (Integer) Utils.getPrefsInt(app.getPrefs(), key, getDef(key)); case PREF_BOOL: - return (T) (Boolean) app.prefs.getBoolean(key, getDef(key)); + return (T) (Boolean) app.getPrefs().getBoolean(key, getDef(key)); case PREF_STR: - return (T) app.prefs.getString(key, getDef(key)); + return (T) app.getPrefs().getString(key, getDef(key)); case DB_INT: - return (T) (Integer) app.mDB.getSettings(key, getDef(key)); + return (T) (Integer) app.getDB().getSettings(key, getDef(key)); 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: - return (T) app.mDB.getStrings(key, getDef(key)); + return (T) app.getDB().getStrings(key, getDef(key)); } /* Will never get here (IllegalArgumentException in getConfigType) */ return null; @@ -260,25 +260,25 @@ public class Config { App app = App.self; switch (getConfigType(key)) { case PREF_INT: - app.prefs.edit().putInt(key, (int) val).apply(); + app.getPrefs().edit().putInt(key, (int) val).apply(); break; case PREF_STR_INT: - app.prefs.edit().putString(key, String.valueOf(val)).apply(); + app.getPrefs().edit().putString(key, String.valueOf(val)).apply(); break; case PREF_BOOL: - app.prefs.edit().putBoolean(key, (boolean) val).apply(); + app.getPrefs().edit().putBoolean(key, (boolean) val).apply(); break; case PREF_STR: - app.prefs.edit().putString(key, (String) val).apply(); + app.getPrefs().edit().putString(key, (String) val).apply(); break; case DB_INT: - app.mDB.setSettings(key, (int) val); + app.getDB().setSettings(key, (int) val); break; case DB_BOOL: - app.mDB.setSettings(key, (boolean) val ? 1 : 0); + app.getDB().setSettings(key, (boolean) val ? 1 : 0); break; case DB_STR: - app.mDB.setStrings(key, (String) val); + app.getDB().setStrings(key, (String) val); break; } } @@ -290,14 +290,14 @@ public class Config { case PREF_STR_INT: case PREF_BOOL: case PREF_STR: - app.prefs.edit().remove(key).apply(); + app.getPrefs().edit().remove(key).apply(); break; case DB_BOOL: case DB_INT: - app.mDB.rmSettings(key); + app.getDB().rmSettings(key); break; case DB_STR: - app.mDB.setStrings(key, null); + app.getDB().setStrings(key, null); break; } } @@ -365,13 +365,13 @@ public class Config { switch (type) { case DB_INT: editor.putString(key, String.valueOf( - app.mDB.getSettings(key, (Integer) defs.get(key)))); + app.getDB().getSettings(key, (Integer) defs.get(key)))); continue; 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; 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); continue; } diff --git a/app/src/main/java/com/topjohnwu/magisk/Const.java b/app/src/main/java/com/topjohnwu/magisk/Const.java index 311283b70..5189cda47 100644 --- a/app/src/main/java/com/topjohnwu/magisk/Const.java +++ b/app/src/main/java/com/topjohnwu/magisk/Const.java @@ -35,6 +35,9 @@ public class Const { 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 int MIN_SUPPORT = 18000; } diff --git a/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt new file mode 100644 index 000000000..861f1ce27 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt @@ -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().resources } + single { get() as App } + single { get().packageManager } + single(SUTimeout) { + get().protectedContext + .getSharedPreferences("su_timeout", 0) + } + single { PreferenceManager.getDefaultSharedPreferences(get().protectedContext) } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt new file mode 100644 index 000000000..0f79c302c --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt @@ -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().protectedContext) } + single { RepoDatabaseHelper(get()) } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/di/MiscModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/MiscModule.kt new file mode 100644 index 000000000..037a9edd6 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/di/MiscModule.kt @@ -0,0 +1,10 @@ +package com.topjohnwu.magisk.di + +import org.koin.dsl.module + + +val miscModule = module { + + // define miscs here + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/di/Modules.kt b/app/src/main/java/com/topjohnwu/magisk/di/Modules.kt new file mode 100644 index 000000000..850e09c88 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/di/Modules.kt @@ -0,0 +1,10 @@ +package com.topjohnwu.magisk.di + +val koinModules = listOf( + applicationModule, + networkingModule, + databaseModule, + repositoryModule, + viewModelModules, + miscModule +) diff --git a/app/src/main/java/com/topjohnwu/magisk/di/NamedInjection.kt b/app/src/main/java/com/topjohnwu/magisk/di/NamedInjection.kt new file mode 100644 index 000000000..c6ab30319 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/di/NamedInjection.kt @@ -0,0 +1,5 @@ +package com.topjohnwu.magisk.di + +import org.koin.core.qualifier.named + +val SUTimeout = named("su_timeout") \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt new file mode 100644 index 000000000..e3cd89d17 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt @@ -0,0 +1,6 @@ +package com.topjohnwu.magisk.di + +import org.koin.dsl.module + + +val networkingModule = module {} diff --git a/app/src/main/java/com/topjohnwu/magisk/di/RepositoryModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/RepositoryModule.kt new file mode 100644 index 000000000..0d55b410c --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/di/RepositoryModule.kt @@ -0,0 +1,6 @@ +package com.topjohnwu.magisk.di + +import org.koin.dsl.module + + +val repositoryModule = module {} diff --git a/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt new file mode 100644 index 000000000..0c59ce6cf --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt @@ -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()) } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/adapters/ApplicationAdapter.java b/app/src/main/java/com/topjohnwu/magisk/model/adapters/ApplicationAdapter.java deleted file mode 100644 index 3eaa65383..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/adapters/ApplicationAdapter.java +++ /dev/null @@ -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 - { - - 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 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 DEFAULT_HIDELIST = Lists.of( - SAFETYNET_PROCESS - ); - - private static int BOTTOM_MARGIN = -1; - - private List fullList, showList; - private List 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 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 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 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 { - String name; - ApplicationInfo info; - List processList; - boolean expanded; - IndeterminateCheckBox.OnStateChangedListener listener; - - HideAppInfo(ApplicationInfo appInfo, Set 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 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 { - 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 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); - } - } - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/adapters/ModulesAdapter.java b/app/src/main/java/com/topjohnwu/magisk/model/adapters/ModulesAdapter.java deleted file mode 100644 index 59dc327cd..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/adapters/ModulesAdapter.java +++ /dev/null @@ -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 { - - private final List mList; - - public ModulesAdapter(List 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); - } - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/adapters/PolicyAdapter.java b/app/src/main/java/com/topjohnwu/magisk/model/adapters/PolicyAdapter.java deleted file mode 100644 index 71f4a0bc1..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/adapters/PolicyAdapter.java +++ /dev/null @@ -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 { - - private List policyList; - private MagiskDB dbHelper; - private PackageManager pm; - private boolean[] expandList; - - public PolicyAdapter(List 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); - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/adapters/ReposAdapter.java b/app/src/main/java/com/topjohnwu/magisk/model/adapters/ReposAdapter.java deleted file mode 100644 index 1541c76d1..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/adapters/ReposAdapter.java +++ /dev/null @@ -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 - 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 moduleMap; - private RepoDatabaseHelper repoDB; - private List>> repoPairs; - private List 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 updates = new ArrayList<>(); - List installed = new ArrayList<>(); - List 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); - } - - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/adapters/SectionedAdapter.java b/app/src/main/java/com/topjohnwu/magisk/model/adapters/SectionedAdapter.java deleted file mode 100644 index e04ec20c7..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/adapters/SectionedAdapter.java +++ /dev/null @@ -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 - extends RecyclerView.Adapter { - - 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); -} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/adapters/StringListAdapter.java b/app/src/main/java/com/topjohnwu/magisk/model/adapters/StringListAdapter.java deleted file mode 100644 index 300ff1658..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/adapters/StringListAdapter.java +++ /dev/null @@ -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 - extends RecyclerView.Adapter { - - private RecyclerView rv; - private boolean dynamic; - private int screenWidth; - private int txtWidth = -1; - private int padding; - - protected List mList; - - public StringListAdapter(List list) { - this(list, false); - } - - public StringListAdapter(List 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(); - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/adapters/SuLogAdapter.java b/app/src/main/java/com/topjohnwu/magisk/model/adapters/SuLogAdapter.java deleted file mode 100644 index 1a43172bb..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/adapters/SuLogAdapter.java +++ /dev/null @@ -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 { - - private List> logEntries; - private Set 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); - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/adapters/TabFragmentAdapter.java b/app/src/main/java/com/topjohnwu/magisk/model/adapters/TabFragmentAdapter.java deleted file mode 100644 index aece29667..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/adapters/TabFragmentAdapter.java +++ /dev/null @@ -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 fragmentList; - private List 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); - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/download/DownloadModuleService.java b/app/src/main/java/com/topjohnwu/magisk/model/download/DownloadModuleService.java index ceb3ea494..324b29734 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/download/DownloadModuleService.java +++ b/app/src/main/java/com/topjohnwu/magisk/model/download/DownloadModuleService.java @@ -6,8 +6,6 @@ import android.content.Intent; import android.net.Uri; import android.os.IBinder; -import androidx.annotation.Nullable; - import com.topjohnwu.magisk.App; import com.topjohnwu.magisk.ClassMap; import com.topjohnwu.magisk.Const; @@ -31,6 +29,8 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; +import androidx.annotation.Nullable; + public class DownloadModuleService extends Service { private List notifications; diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/BaseModule.java b/app/src/main/java/com/topjohnwu/magisk/model/entity/BaseModule.java index ba746cb97..20ccc4bec 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/BaseModule.java +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/BaseModule.java @@ -5,10 +5,10 @@ import android.database.Cursor; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.NonNull; - import java.util.List; +import androidx.annotation.NonNull; + public abstract class BaseModule implements Comparable, Parcelable { private String mId, mName, mVersion, mAuthor, mDescription; diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/HideAppInfo.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/HideAppInfo.kt new file mode 100644 index 000000000..74e58ed48 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/HideAppInfo.kt @@ -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) + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/HideTarget.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/HideTarget.kt new file mode 100644 index 000000000..27573783c --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/HideTarget.kt @@ -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 } + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/Policy.java b/app/src/main/java/com/topjohnwu/magisk/model/entity/Policy.java index adc54c8fa..10cc6dac6 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/Policy.java +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/Policy.java @@ -4,10 +4,10 @@ import android.content.ContentValues; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import androidx.annotation.NonNull; - import com.topjohnwu.magisk.utils.Utils; +import androidx.annotation.NonNull; + public class Policy implements Comparable{ public static final int INTERACTIVE = 0; diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ConsoleRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ConsoleRvItem.kt new file mode 100644 index 000000000..af4e508a8 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ConsoleRvItem.kt @@ -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() { + override val layoutRes: Int = R.layout.item_console + + override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other) + override fun itemSameAs(other: ConsoleRvItem) = item == other.item +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HideRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HideRvItem.kt new file mode 100644 index 000000000..61cf4de91 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HideRvItem.kt @@ -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) : + ComparableRvItem() { + + 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() + + 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() { + + 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt new file mode 100644 index 000000000..ea2b03537 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt @@ -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() { + override val layoutRes: Int = R.layout.item_page_log + + val items = DiffObservableList(callback) + + fun update(list: List) { + 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() { + override val layoutRes: Int = R.layout.item_superuser_log + + val date = items.filterIsInstance().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() { + 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() { + override val layoutRes: Int = R.layout.item_page_magisk_log + + val items = DiffObservableList(callback) + + fun update(list: List) { + 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt new file mode 100644 index 000000000..74c90a814 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt @@ -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() { + + 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().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() { + + 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/PolicyRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/PolicyRvItem.kt new file mode 100644 index 000000000..c38c8881c --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/PolicyRvItem.kt @@ -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() { + + 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SectionRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SectionRvItem.kt new file mode 100644 index 000000000..11fd9ebe3 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SectionRvItem.kt @@ -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() { + override val layoutRes: Int = R.layout.item_section + + override fun contentSameAs(other: SectionRvItem) = itemSameAs(other) + override fun itemSameAs(other: SectionRvItem) = text == other.text +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SpinnerRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SpinnerRvItem.kt new file mode 100644 index 000000000..92dcbf08f --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SpinnerRvItem.kt @@ -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() { + + override val layoutRes: Int = R.layout.item_spinner + + override fun contentSameAs(other: SpinnerRvItem) = itemSameAs(other) + override fun itemSameAs(other: SpinnerRvItem) = item == other.item + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/state/IndeterminateState.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/state/IndeterminateState.kt new file mode 100644 index 000000000..ba381e695 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/state/IndeterminateState.kt @@ -0,0 +1,5 @@ +package com.topjohnwu.magisk.model.entity.state + +enum class IndeterminateState { + INDETERMINATE, CHECKED, UNCHECKED +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/RxEvents.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/RxEvents.kt new file mode 100644 index 000000000..e713d4ece --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/RxEvents.kt @@ -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 diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt new file mode 100644 index 000000000..a47adff93 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt @@ -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, + val callback: PublishSubject +) : ViewEvent() + +class BackPressEvent : ViewEvent() + +class SuDialogEvent(val policy: Policy) : ViewEvent() +class DieEvent : ViewEvent() \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/flash/FlashResultListener.kt b/app/src/main/java/com/topjohnwu/magisk/model/flash/FlashResultListener.kt new file mode 100644 index 000000000..f89d87f12 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/flash/FlashResultListener.kt @@ -0,0 +1,7 @@ +package com.topjohnwu.magisk.model.flash + +interface FlashResultListener { + + fun onResult(isSuccess: Boolean) + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt b/app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt new file mode 100644 index 000000000..858ddcc8c --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt @@ -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, + log: MutableList, + 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, + log: MutableList, + 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, + log: MutableList, + 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) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt b/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt new file mode 100644 index 000000000..ebb21128e --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt @@ -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, + logs: MutableList, + 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, + logs: MutableList, + resultListener: FlashResultListener + ) : Patching(console, logs, resultListener) { + override fun operations() = + extractZip() && handleFile(uri) && patchBoot() && storeBoot() + } + + class SecondSlot( + console: MutableList, + logs: MutableList, + resultListener: FlashResultListener + ) : Patching(console, logs, resultListener) { + override fun operations() = + findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA() + } + + class Direct( + console: MutableList, + logs: MutableList, + resultListener: FlashResultListener + ) : Patching(console, logs, resultListener) { + override fun operations() = + findImage() && extractZip() && patchBoot() && flashBoot() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/navigation/MagiskNavigationEvent.kt b/app/src/main/java/com/topjohnwu/magisk/model/navigation/MagiskNavigationEvent.kt new file mode 100644 index 000000000..793c2a4c2 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/navigation/MagiskNavigationEvent.kt @@ -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? = 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigation.kt b/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigation.kt new file mode 100644 index 000000000..361c79cb9 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigation.kt @@ -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 + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigator.kt b/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigator.kt new file mode 100644 index 000000000..e2b755a9d --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigator.kt @@ -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> + + fun navigateTo(event: MagiskNavigationEvent) + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/observer/Observer.kt b/app/src/main/java/com/topjohnwu/magisk/model/observer/Observer.kt new file mode 100644 index 000000000..3f675dded --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/observer/Observer.kt @@ -0,0 +1,31 @@ +package com.topjohnwu.magisk.model.observer + +import androidx.databinding.Observable +import androidx.databinding.ObservableField +import java.io.Serializable + + +class Observer(vararg dependencies: Observable, private val observer: () -> T) : + ObservableField(*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)" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/permissions/PermissionRequestBuilder.kt b/app/src/main/java/com/topjohnwu/magisk/model/permissions/PermissionRequestBuilder.kt new file mode 100644 index 000000000..7aea7d587 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/permissions/PermissionRequestBuilder.kt @@ -0,0 +1,40 @@ +package com.topjohnwu.magisk.model.permissions + +typealias SimpleCallback = () -> Unit +typealias PermissionRationaleCallback = (List) -> 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) = onShowRationaleCallback(permissions) + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/receiver/GeneralReceiver.java b/app/src/main/java/com/topjohnwu/magisk/model/receiver/GeneralReceiver.java index dd71c7f09..0a7432c4d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/receiver/GeneralReceiver.java +++ b/app/src/main/java/com/topjohnwu/magisk/model/receiver/GeneralReceiver.java @@ -61,12 +61,12 @@ public class GeneralReceiver extends BroadcastReceiver { case Intent.ACTION_PACKAGE_REPLACED: // This will only work pre-O if (Config.get(Config.Key.SU_REAUTH)) { - app.mDB.deletePolicy(getPkg(intent)); + app.getDB().deletePolicy(getPkg(intent)); } break; case Intent.ACTION_PACKAGE_FULLY_REMOVED: String pkg = getPkg(intent); - app.mDB.deletePolicy(pkg); + app.getDB().deletePolicy(pkg); Shell.su("magiskhide --rm " + pkg).submit(); break; case Intent.ACTION_LOCALE_CHANGED: diff --git a/app/src/main/java/com/topjohnwu/magisk/model/worker/DelegateWorker.java b/app/src/main/java/com/topjohnwu/magisk/model/worker/DelegateWorker.java index f9460e824..670e88d1c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/worker/DelegateWorker.java +++ b/app/src/main/java/com/topjohnwu/magisk/model/worker/DelegateWorker.java @@ -4,6 +4,12 @@ import android.content.Context; import android.net.Network; 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.NonNull; import androidx.annotation.Nullable; @@ -11,12 +17,6 @@ import androidx.annotation.RequiresApi; import androidx.work.Data; 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 { private ListenableWorker worker; diff --git a/app/src/main/java/com/topjohnwu/magisk/tasks/UpdateRepos.java b/app/src/main/java/com/topjohnwu/magisk/tasks/UpdateRepos.java index 9d6e44571..d7d4754a5 100644 --- a/app/src/main/java/com/topjohnwu/magisk/tasks/UpdateRepos.java +++ b/app/src/main/java/com/topjohnwu/magisk/tasks/UpdateRepos.java @@ -34,7 +34,7 @@ import java.util.concurrent.Future; public class UpdateRepos { private static final DateFormat DATE_FORMAT; - private App app = App.self; + private final App app = App.self; private Set cached; private Queue> moduleQueue; @@ -116,17 +116,17 @@ public class UpdateRepos { Pair pair = moduleQueue.poll(); if (pair == null) return; - Repo repo = app.repoDB.getRepo(pair.first); + Repo repo = app.getRepoDB().getRepo(pair.first); try { if (repo == null) repo = new Repo(pair.first); else cached.remove(pair.first); repo.update(pair.second); - app.repoDB.addRepo(repo); + app.getRepoDB().addRepo(repo); } catch (Repo.IllegalRepoException e) { Logger.debug(e.getMessage()); - app.repoDB.removeRepo(pair.first); + app.getRepoDB().removeRepo(pair.first); } } }); @@ -134,7 +134,7 @@ public class UpdateRepos { } private void fullReload() { - Cursor c = app.repoDB.getRawCursor(); + Cursor c = app.getRepoDB().getRawCursor(); runTasks(() -> { while (true) { Repo repo; @@ -145,10 +145,10 @@ public class UpdateRepos { } try { repo.update(); - app.repoDB.addRepo(repo); + app.getRepoDB().addRepo(repo); } catch (Repo.IllegalRepoException e) { Logger.debug(e.getMessage()); - app.repoDB.removeRepo(repo); + app.getRepoDB().removeRepo(repo); } } }); @@ -157,12 +157,12 @@ public class UpdateRepos { public void exec(boolean force) { Event.reset(Event.REPO_LOAD_DONE); App.THREAD_POOL.execute(() -> { - cached = Collections.synchronizedSet(app.repoDB.getRepoIDSet()); + cached = Collections.synchronizedSet(app.getRepoDB().getRepoIDSet()); moduleQueue = new ConcurrentLinkedQueue<>(); if (loadPages()) { // The leftover cached means they are removed from online repo - app.repoDB.removeRepo(cached); + app.getRepoDB().removeRepo(cached); } else if (force) { fullReload(); } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.java b/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.java deleted file mode 100644 index 1f566a259..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.java +++ /dev/null @@ -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); - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt new file mode 100644 index 000000000..6ab8b1e81 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt @@ -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() { + + 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> = 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(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(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() + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt new file mode 100644 index 000000000..73d8b80c8 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt @@ -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 + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.java b/app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.java deleted file mode 100644 index 09732bb8d..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.java +++ /dev/null @@ -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(); - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt new file mode 100644 index 000000000..d3f862447 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt @@ -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(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(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 + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/ActivityResultListener.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/ActivityResultListener.kt new file mode 100644 index 000000000..16d963403 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/ActivityResultListener.kt @@ -0,0 +1,7 @@ +package com.topjohnwu.magisk.ui.base + +import android.content.Intent + +interface ActivityResultListener { + fun onActivityResult(resultCode: Int, data: Intent?) +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseActivity.java b/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseActivity.java deleted file mode 100644 index cdefa4e7c..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseActivity.java +++ /dev/null @@ -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 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); - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseFragment.java deleted file mode 100644 index 93cbd6629..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseFragment.java +++ /dev/null @@ -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) {} -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/BasePreferenceFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/base/BasePreferenceFragment.java index 2daa57f6e..8ae2bb7a1 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/BasePreferenceFragment.java +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/BasePreferenceFragment.java @@ -8,6 +8,10 @@ import android.view.LayoutInflater; import android.view.View; 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.PreferenceCategory; import androidx.preference.PreferenceFragmentCompat; @@ -16,10 +20,6 @@ import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; 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 implements SharedPreferences.OnSharedPreferenceChangeListener, Event.AutoListener { @@ -28,21 +28,21 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = super.onCreateView(inflater, container, savedInstanceState); - app.prefs.registerOnSharedPreferenceChangeListener(this); + app.getPrefs().registerOnSharedPreferenceChangeListener(this); Event.register(this); return v; } @Override public void onDestroyView() { - app.prefs.unregisterOnSharedPreferenceChangeListener(this); + app.getPrefs().unregisterOnSharedPreferenceChangeListener(this); Event.unregister(this); super.onDestroyView(); } @Override public int[] getListeningEvents() { - return BaseActivity.EMPTY_INT_ARRAY; + return new int[0]; } @Override diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/IBaseLeanback.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/IBaseLeanback.kt new file mode 100644 index 000000000..8008284a4 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/IBaseLeanback.kt @@ -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) + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt new file mode 100644 index 000000000..71f66a7ff --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt @@ -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 : + MagiskLeanbackActivity(), FragNavController.RootFragmentListener, + Navigator { + + override val numberOfRootFragments: Int get() = baseFragments.size + override val baseFragments: List> = 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(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?, + 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) + } + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt new file mode 100644 index 000000000..a05134d16 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt @@ -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 : + TeanityFragment(), Navigator { + + protected val magiskActivity get() = activity as MagiskActivity<*, *> + + // We don't need nested fragments + override val baseFragments: List> = 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 + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskLeanbackActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskLeanbackActivity.kt new file mode 100644 index 000000000..faaec67f5 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskLeanbackActivity.kt @@ -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 : + TeanityActivity(), IBaseLeanback { + + private val resultListeners = SparseArrayCompat() + + @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?, + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt new file mode 100644 index 000000000..3b1663958 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt @@ -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 { + val subject = PublishSubject.create() + return subject.doOnSubscribe { PermissionEvent(permissions.toList(), subject).publish() } + } + + fun back() = BackPressEvent().publish() + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.java b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.java deleted file mode 100644 index ba1c54aa1..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.java +++ /dev/null @@ -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 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() { - 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 { - - 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(); - } - } - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.kt new file mode 100644 index 000000000..48579b832 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.kt @@ -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() { + + 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() + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt new file mode 100644 index 000000000..e0413d233 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt @@ -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> { itemBinding, _, item -> + item.bind(itemBinding) + itemBinding.bindExtra(BR.viewModel, this@FlashViewModel) + } + + private val rawItems = ObservableArrayList() + + 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() + .joinToString("\n") { it.item } + file.writeText(log) + file.path + } + .subscribeK { SnackbarEvent(it).publish() } + .add() + + fun restartPressed() = RootUtils.reboot() + + fun backPressed() = back() + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt new file mode 100644 index 000000000..897196b3a --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt @@ -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>() + val items = DiffObservableList(ComparableRvItem.callback) + val itemBinding = OnItemBind> { itemBinding, _, item -> + item.bind(itemBinding) + itemBinding.bindExtra(BR.viewModel, this@HideViewModel) + } + + init { + rxBus.register() + .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() } + .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" + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/hide/MagiskHideFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/hide/MagiskHideFragment.java deleted file mode 100644 index 1bb686ffe..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/hide/MagiskHideFragment.java +++ /dev/null @@ -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()); - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/hide/MagiskHideFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/hide/MagiskHideFragment.kt new file mode 100644 index 000000000..64f5f0885 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/hide/MagiskHideFragment.kt @@ -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(), + 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(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()) + }*/ +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt new file mode 100644 index 000000000..f5d16d2ab --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt @@ -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(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)" + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/home/MagiskFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/home/MagiskFragment.java deleted file mode 100644 index 8dbc449a3..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/home/MagiskFragment.java +++ /dev/null @@ -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(); - } - } -} - diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/home/MagiskFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/home/MagiskFragment.kt new file mode 100644 index 000000000..11a4d8554 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/home/MagiskFragment.kt @@ -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(), + 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) + } + } +} + diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/home/MagiskItem.kt b/app/src/main/java/com/topjohnwu/magisk/ui/home/MagiskItem.kt new file mode 100644 index 000000000..8f26b0c1d --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/home/MagiskItem.kt @@ -0,0 +1,6 @@ +package com.topjohnwu.magisk.ui.home + + +enum class MagiskItem { + MANAGER, MAGISK +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/home/MagiskState.kt b/app/src/main/java/com/topjohnwu/magisk/ui/home/MagiskState.kt new file mode 100644 index 000000000..8d880001d --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/home/MagiskState.kt @@ -0,0 +1,6 @@ +package com.topjohnwu.magisk.ui.home + + +enum class MagiskState { + NO_ROOT, NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/home/SafetyNetState.kt b/app/src/main/java/com/topjohnwu/magisk/ui/home/SafetyNetState.kt new file mode 100644 index 000000000..fc0df345d --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/home/SafetyNetState.kt @@ -0,0 +1,5 @@ +package com.topjohnwu.magisk.ui.home + +enum class SafetyNetState { + LOADING, PASS, FAILED, IDLE +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.java deleted file mode 100644 index 7757d743d..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.kt new file mode 100644 index 000000000..9db8f2a67 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.kt @@ -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() { + + 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 + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt new file mode 100644 index 000000000..0d93c32e9 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt @@ -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> { + + val items = DiffObservableList(ComparableRvItem.callback) + val itemBinding = OnItemBind> { 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>().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) } + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/log/MagiskLogFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/log/MagiskLogFragment.java deleted file mode 100644 index 231b1a161..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/log/MagiskLogFragment.java +++ /dev/null @@ -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(List 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; - } - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/log/SuLogFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/log/SuLogFragment.java deleted file mode 100644 index 31588c26d..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/log/SuLogFragment.java +++ /dev/null @@ -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; - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt new file mode 100644 index 000000000..031d67c3c --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt @@ -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>() + + val itemsInstalled = DiffObservableList(ComparableRvItem.callback) + val itemsRemote = DiffObservableList(ComparableRvItem.callback) + val itemBinding = OnItemBind> { 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) = 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() } + .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.divide(): List> { + val installed = itemsInstalled.filterIsInstance() + val installedModules = filter { installed.any { item -> it.item.id == item.item.id } } + + fun installedByID(id: String) = installed.firstOrNull { it.item.id == id } + + fun List.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): List> { + 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 Cursor.toList(transformer: (Cursor) -> Result): List { + val out = mutableListOf() + while (moveToNext()) out.add(transformer(this)) + return out + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.java deleted file mode 100644 index d217b0e73..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.java +++ /dev/null @@ -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 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 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); - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.kt new file mode 100644 index 000000000..9aff75076 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.kt @@ -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() { + + 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(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) { + 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 + }*/ +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.java deleted file mode 100644 index 2048d5dc1..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt new file mode 100644 index 000000000..51b6d8830 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt @@ -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(), + 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(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(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() + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.java index f74a61f50..90faae656 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.java +++ b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.java @@ -8,13 +8,6 @@ import android.view.View; import android.widget.EditText; 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.Config; import com.topjohnwu.magisk.Const; @@ -35,18 +28,32 @@ import java.io.IOException; import java.util.Arrays; 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 { private ListPreference updateChannel, autoRes, suNotification, requestTimeout, rootConfig, multiuserConfig, nsConfig; @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - setPreferencesFromResource(R.xml.app_settings, rootKey); + public void onStart() { + super.onStart(); + setHasOptionsMenu(true); 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(); - app.prefs.edit() + app.getPrefs().edit() .putBoolean(Config.Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint()) .apply(); @@ -66,8 +73,8 @@ public class SettingsFragment extends BasePreferenceFragment { return true; }); findPreference("clear").setOnPreferenceClickListener(pref -> { - app.prefs.edit().remove(Config.Key.ETAG_KEY).apply(); - app.repoDB.clearRepo(); + app.getPrefs().edit().remove(Config.Key.ETAG_KEY).apply(); + app.getRepoDB().clearRepo(); Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT); return true; }); @@ -94,12 +101,12 @@ public class SettingsFragment extends BasePreferenceFragment { if (channel == Config.Value.CUSTOM_CHANNEL) { View v = LayoutInflater.from(requireActivity()).inflate(R.layout.custom_channel_dialog, null); 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()) .setTitle(R.string.settings_update_custom) .setView(v) .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) -> Config.set(Config.Key.UPDATE_CHANNEL, prev)) .setOnCancelListener(d -> @@ -183,7 +190,7 @@ public class SettingsFragment extends BasePreferenceFragment { case Config.Key.ROOT_ACCESS: case Config.Key.SU_MULTIUSER_MODE: case Config.Key.SU_MNT_NS: - app.mDB.setSettings(key, Utils.getPrefsInt(prefs, key)); + app.getDB().setSettings(key, Utils.getPrefsInt(prefs, key)); break; case Config.Key.DARK_THEME: requireActivity().recreate(); diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.java deleted file mode 100644 index 775b00217..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.java +++ /dev/null @@ -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 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); - } - } - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.kt new file mode 100644 index 000000000..1547fec3e --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.kt @@ -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() { + + 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() + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt new file mode 100644 index 000000000..c083861c2 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt @@ -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> { itemBinding, _, item -> + item.bind(itemBinding) + itemBinding.bindExtra(BR.viewModel, this@SuperuserViewModel) + } + + private var ignoreNext: PolicyRvItem? = null + + init { + rxBus.register() + .subscribeK { togglePolicy(it.item, it.enable) } + .add() + rxBus.register() + .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().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() + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.java b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.java deleted file mode 100644 index b3ea3aee0..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.java +++ /dev/null @@ -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 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 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(); - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt new file mode 100644 index 000000000..9ad1b2f62 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt @@ -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() { + + 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" + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt new file mode 100644 index 000000000..a3f67743b --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt @@ -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(null) + val title = KObservableField("") + val packageName = KObservableField("") + + val denyText = KObservableField(resources.getString(R.string.deny)) + val warningText = KObservableField(resources.getString(R.string.su_warning)) + + val canUseFingerprint = KObservableField(FingerprintHelper.useFingerprint()) + val selectedItemPosition = KObservableField(0) + + val items = DiffObservableList(ComparableRvItem.callback) + val itemBinding = ItemBinding.of> { 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(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(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() + + 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() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestActivity.kt new file mode 100644 index 000000000..4bfefa221 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestActivity.kt @@ -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(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" + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestViewModel.kt new file mode 100644 index 000000000..4ac1c1ea0 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestViewModel.kt @@ -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 = 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 = 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(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() + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt new file mode 100644 index 000000000..d6cc4d32f --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.java b/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.java index fb7c8e678..0d77c058a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.java +++ b/app/src/main/java/com/topjohnwu/magisk/utils/PatchAPK.java @@ -3,8 +3,6 @@ package com.topjohnwu.magisk.utils; import android.content.ComponentName; import android.widget.Toast; -import androidx.core.app.NotificationCompat; - import com.topjohnwu.magisk.App; import com.topjohnwu.magisk.BuildConfig; import com.topjohnwu.magisk.ClassMap; @@ -29,6 +27,8 @@ import java.util.ArrayList; import java.util.List; import java.util.jar.JarEntry; +import androidx.core.app.NotificationCompat; + public class PatchAPK { public static final String LOWERALPHA = "abcdefghijklmnopqrstuvwxyz"; diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/SuLogger.java b/app/src/main/java/com/topjohnwu/magisk/utils/SuLogger.java index 888a65262..17d7c9286 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/SuLogger.java +++ b/app/src/main/java/com/topjohnwu/magisk/utils/SuLogger.java @@ -37,7 +37,7 @@ public class SuLogger { } } else { // Doesn't report whether notify or not, check database ourselves - policy = app.mDB.getPolicy(fromUid); + policy = app.getDB().getPolicy(fromUid); if (policy == null) return; notify = policy.notification; @@ -62,7 +62,7 @@ public class SuLogger { log.fromPid = pid; log.command = command; log.date = new Date(); - app.mDB.addLog(log); + app.getDB().addLog(log); } private static void handleNotify(Policy policy) { diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java b/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java index 2d73d2cf0..1cbcb06d5 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java +++ b/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java @@ -12,12 +12,6 @@ import android.net.Uri; import android.provider.OpenableColumns; 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.BuildConfig; import com.topjohnwu.magisk.ClassMap; @@ -36,6 +30,12 @@ import java.util.Locale; import java.util.Map; 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 static void toast(CharSequence msg, int duration) { diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/XAndroid.kt b/app/src/main/java/com/topjohnwu/magisk/utils/XAndroid.kt new file mode 100644 index 000000000..bf594c755 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/XAndroid.kt @@ -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.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 \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/XBinding.kt b/app/src/main/java/com/topjohnwu/magisk/utils/XBinding.kt new file mode 100644 index 000000000..ff9307a9e --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/XBinding.kt @@ -0,0 +1,8 @@ +package com.topjohnwu.magisk.utils + +import com.skoumal.teanity.util.KObservableField + + +fun KObservableField.toggle() { + value = !value +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/XKoin.kt b/app/src/main/java/com/topjohnwu/magisk/utils/XKoin.kt new file mode 100644 index 000000000..951dd163d --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/XKoin.kt @@ -0,0 +1,20 @@ +package com.topjohnwu.magisk.utils + +import org.koin.core.context.GlobalContext +import org.koin.core.parameter.ParametersDefinition +import org.koin.core.qualifier.Qualifier +import org.koin.core.scope.Scope + +fun getKoin() = GlobalContext.get().koin + +inline fun inject( + qualifier: Qualifier? = null, + scope: Scope? = null, + noinline parameters: ParametersDefinition? = null +) = lazy { get(qualifier, scope, parameters) } + +inline fun get( + qualifier: Qualifier? = null, + scope: Scope? = null, + noinline parameters: ParametersDefinition? = null +): T = getKoin().get(qualifier, scope, parameters) \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/XList.kt b/app/src/main/java/com/topjohnwu/magisk/utils/XList.kt new file mode 100644 index 000000000..705ee9bb3 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/XList.kt @@ -0,0 +1,49 @@ +package com.topjohnwu.magisk.utils + +import androidx.databinding.ObservableList +import com.skoumal.teanity.extensions.subscribeK +import com.skoumal.teanity.util.DiffObservableList +import io.reactivex.disposables.Disposable + +fun MutableList.update(newList: List) { + clear() + addAll(newList) +} + +fun ObservableList.sendUpdatesTo( + target: DiffObservableList, + mapper: (List) -> List +) { + addOnListChangedCallback(object : + ObservableList.OnListChangedCallback>() { + override fun onChanged(sender: ObservableList?) { + updateAsync(sender ?: return) + } + + override fun onItemRangeRemoved(sender: ObservableList?, p0: Int, p1: Int) { + updateAsync(sender ?: return) + } + + override fun onItemRangeMoved(sender: ObservableList?, p0: Int, p1: Int, p2: Int) { + updateAsync(sender ?: return) + } + + override fun onItemRangeInserted(sender: ObservableList?, p0: Int, p1: Int) { + updateAsync(sender ?: return) + } + + override fun onItemRangeChanged(sender: ObservableList?, p0: Int, p1: Int) { + updateAsync(sender ?: return) + } + + private var updater: Disposable? = null + + private fun updateAsync(sender: List) { + updater?.dispose() + updater = sender.toSingle() + .map { mapper(it) } + .map { it to target.calculateDiff(it) } + .subscribeK { target.update(it.first, it.second) } + } + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/XRx.kt b/app/src/main/java/com/topjohnwu/magisk/utils/XRx.kt new file mode 100644 index 000000000..62a324c6c --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/XRx.kt @@ -0,0 +1,9 @@ +package com.topjohnwu.magisk.utils + +import io.reactivex.Single +import io.reactivex.functions.BiFunction + +fun T.toSingle() = Single.just(this) + +fun zip(t1: Single, t2: Single, zipper: (T1, T2) -> R) = + Single.zip(t1, t2, BiFunction { rt1, rt2 -> zipper(rt1, rt2) }) \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/XString.kt b/app/src/main/java/com/topjohnwu/magisk/utils/XString.kt new file mode 100644 index 000000000..5f41b8693 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/XString.kt @@ -0,0 +1,11 @@ +package com.topjohnwu.magisk.utils + +val specialChars = arrayOf('!', '@', '#', '$', '%', '&', '?') + +fun String.replaceRandomWithSpecial(): String { + var random: Char + do { + random = random() + } while (random == '.') + return replace(random, specialChars.random()) +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/XTime.kt b/app/src/main/java/com/topjohnwu/magisk/utils/XTime.kt new file mode 100644 index 000000000..b2588a540 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/XTime.kt @@ -0,0 +1,18 @@ +package com.topjohnwu.magisk.utils + +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +val now get() = System.currentTimeMillis() + +fun Long.toTime(format: SimpleDateFormat) = format.format(this).orEmpty() +fun String.toTime(format: SimpleDateFormat) = try { + format.parse(this)?.time ?: -1 +} catch (e: ParseException) { + -1L +} + +private val locale get() = Locale.getDefault() +val timeFormatFull by lazy { SimpleDateFormat("YYYY/MM/DD_HH:mm:ss", locale) } +val timeFormatStandard by lazy { SimpleDateFormat("YYYY-MM-DD'T'HH:mm:ss'Z'", locale) } \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/feature/Annotations.kt b/app/src/main/java/com/topjohnwu/magisk/utils/feature/Annotations.kt new file mode 100644 index 000000000..2f7fb6361 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/feature/Annotations.kt @@ -0,0 +1,16 @@ +package com.topjohnwu.magisk.utils.feature + +@Retention(AnnotationRetention.SOURCE) +annotation class WIP + + +@Retention(AnnotationRetention.SOURCE) +annotation class Beta + + +@Retention(AnnotationRetention.SOURCE) +annotation class Alpha + + +@Retention(AnnotationRetention.SOURCE) +annotation class Unstable \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/view/ArrowExpandable.java b/app/src/main/java/com/topjohnwu/magisk/view/ArrowExpandable.java deleted file mode 100644 index 81478cddd..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/view/ArrowExpandable.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.topjohnwu.magisk.view; - -import android.view.View; -import android.view.animation.Animation; -import android.view.animation.RotateAnimation; - -public class ArrowExpandable extends Expandable { - protected Expandable mBase; - private View arrow; - - public ArrowExpandable(Expandable base, View arrow) { - mBase = base; - this.arrow = arrow; - } - - @Override - public void onExpand() { - mBase.onExpand(); - setRotate(new RotateAnimation(0, 180, - Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)); - } - - @Override - public void onCollapse() { - mBase.onCollapse(); - setRotate(new RotateAnimation(180, 0, - Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)); - } - - @Override - public void onSetExpanded(boolean expanded) { - mBase.onSetExpanded(expanded); - if (arrow != null) - arrow.setRotation(expanded ? 180 : 0); - } - - private void setRotate(RotateAnimation rotate) { - rotate.setDuration(300); - rotate.setFillAfter(true); - arrow.startAnimation(rotate); - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt b/app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt new file mode 100644 index 000000000..a9ad818ab --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt @@ -0,0 +1,155 @@ +package com.topjohnwu.magisk.view + +import android.content.Context +import android.content.DialogInterface +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.LayoutInflater +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import com.skoumal.teanity.util.KObservableField +import com.topjohnwu.magisk.BR +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.databinding.DialogMagiskBaseBinding + +class MagiskDialog @JvmOverloads constructor( + context: Context, theme: Int = 0 +) : AlertDialog(context, theme) { + + private val binding: DialogMagiskBaseBinding + private val data = Data() + + init { + val layoutInflater = LayoutInflater.from(context) + binding = DataBindingUtil.inflate(layoutInflater, R.layout.dialog_magisk_base, null, false) + binding.setVariable(BR.data, data) + super.setView(binding.root) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + + inner class Data { + val icon = KObservableField(0) + val iconRaw = KObservableField(null) + val title = KObservableField("") + val message = KObservableField("") + + val buttonPositive = Button() + val buttonNeutral = Button() + val buttonNegative = Button() + val buttonIDGAF = Button() + } + + enum class ButtonType { + POSITIVE, NEUTRAL, NEGATIVE, IDGAF + } + + inner class Button { + val icon = KObservableField(0) + val title = KObservableField("") + val isEnabled = KObservableField(true) + + var onClickAction: OnDialogButtonClickListener = {} + + fun clicked() { + onClickAction(this@MagiskDialog) + dismiss() + } + } + + inner class ButtonBuilder(private val button: Button) { + var icon: Int + get() = button.icon.value + set(value) { + button.icon.value = value + } + var title: CharSequence + get() = button.title.value + set(value) { + button.title.value = value + } + var titleRes: Int + get() = 0 + set(value) { + button.title.value = context.getString(value) + } + var isEnabled: Boolean + get() = button.isEnabled.value + set(value) { + button.isEnabled.value = value + } + + fun onClick(listener: OnDialogButtonClickListener) { + button.onClickAction = listener + } + } + + fun applyTitle(@StringRes stringRes: Int) = + apply { data.title.value = context.getString(stringRes) } + + fun applyTitle(title: CharSequence) = + apply { data.title.value = title } + + fun applyMessage(@StringRes stringRes: Int) = + apply { data.message.value = context.getString(stringRes) } + + fun applyMessage(message: CharSequence) = + apply { data.message.value = message } + + fun applyIcon(@DrawableRes drawableRes: Int) = + apply { data.icon.value = drawableRes } + + fun applyIcon(drawable: Drawable) = + apply { data.iconRaw.value = drawable } + + fun applyButton(buttonType: ButtonType, builder: ButtonBuilder.() -> Unit) = apply { + val button = when (buttonType) { + ButtonType.POSITIVE -> data.buttonPositive + ButtonType.NEUTRAL -> data.buttonNeutral + ButtonType.NEGATIVE -> data.buttonNegative + ButtonType.IDGAF -> data.buttonIDGAF + } + ButtonBuilder(button).apply(builder) + } + + fun cancellable(isCancellable: Boolean) = apply { + setCancelable(isCancellable) + } + + fun applyView(binding: Binding, body: Binding.() -> Unit) = + apply { + this.binding.dialogBaseContainer.removeAllViews() + this.binding.dialogBaseContainer.addView(binding.root) + binding.apply(body) + } + + fun onDismiss(callback: OnDialogButtonClickListener) = + apply { setOnDismissListener(callback) } + + fun onShow(callback: OnDialogButtonClickListener) = + apply { setOnShowListener(callback) } + + fun reveal() = apply { super.show() } + + //region Deprecated Members + @Deprecated("Use applyTitle instead", ReplaceWith("applyTitle")) + override fun setTitle(title: CharSequence?) = Unit + + @Deprecated("Use applyTitle instead", ReplaceWith("applyTitle")) + override fun setTitle(titleId: Int) = Unit + + @Deprecated("Use reveal()", ReplaceWith("reveal()")) + override fun show() { + } + //endregion +} + +typealias OnDialogButtonClickListener = (DialogInterface) -> Unit \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.java b/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.java index bcf65c8b3..f6da7a795 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.java +++ b/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.java @@ -1,11 +1,9 @@ package com.topjohnwu.magisk.view; -import android.app.Activity; +import android.content.Context; import android.view.LayoutInflater; import android.view.View; -import androidx.appcompat.app.AlertDialog; - import com.topjohnwu.magisk.R; import com.topjohnwu.net.Networking; import com.topjohnwu.net.ResponseListener; @@ -13,6 +11,7 @@ import com.topjohnwu.net.ResponseListener; import java.io.InputStream; import java.util.Scanner; +import androidx.appcompat.app.AlertDialog; import ru.noties.markwon.Markwon; import ru.noties.markwon.html.HtmlPlugin; import ru.noties.markwon.image.ImagesPlugin; @@ -20,11 +19,11 @@ import ru.noties.markwon.image.svg.SvgPlugin; public class MarkDownWindow { - public static void show(Activity activity, String title, String url) { + public static void show(Context activity, String title, String url) { Networking.get(url).getAsString(new Listener(activity, title)); } - public static void show (Activity activity, String title, InputStream is) { + public static void show(Context activity, String title, InputStream is) { try (Scanner s = new Scanner(is, "UTF-8")) { s.useDelimiter("\\A"); new Listener(activity, title).onResponse(s.next()); @@ -33,10 +32,10 @@ public class MarkDownWindow { static class Listener implements ResponseListener { - Activity activity; + Context activity; String title; - Listener(Activity a, String t) { + Listener(Context a, String t) { activity = a; title = t; } diff --git a/app/src/main/java/com/topjohnwu/magisk/view/Notifications.java b/app/src/main/java/com/topjohnwu/magisk/view/Notifications.java index c8b1d0817..244c51b25 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/Notifications.java +++ b/app/src/main/java/com/topjohnwu/magisk/view/Notifications.java @@ -7,10 +7,6 @@ import android.content.Context; import android.content.Intent; import android.os.Build; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import androidx.core.app.TaskStackBuilder; - import com.topjohnwu.magisk.App; import com.topjohnwu.magisk.ClassMap; import com.topjohnwu.magisk.Config; @@ -20,6 +16,10 @@ import com.topjohnwu.magisk.model.receiver.GeneralReceiver; import com.topjohnwu.magisk.ui.SplashActivity; import com.topjohnwu.magisk.utils.Utils; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.app.TaskStackBuilder; + public class Notifications { public static NotificationManagerCompat mgr = NotificationManagerCompat.from(App.self); diff --git a/app/src/main/java/com/topjohnwu/magisk/view/ProgressNotification.java b/app/src/main/java/com/topjohnwu/magisk/view/ProgressNotification.java index e8424c435..a7ff7721d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/ProgressNotification.java +++ b/app/src/main/java/com/topjohnwu/magisk/view/ProgressNotification.java @@ -5,13 +5,13 @@ import android.app.PendingIntent; import android.content.Intent; import android.widget.Toast; -import androidx.core.app.NotificationCompat; - import com.topjohnwu.magisk.App; import com.topjohnwu.magisk.R; import com.topjohnwu.magisk.utils.Utils; import com.topjohnwu.net.DownloadProgressListener; +import androidx.core.app.NotificationCompat; + public class ProgressNotification implements DownloadProgressListener { private NotificationCompat.Builder builder; diff --git a/app/src/main/java/com/topjohnwu/magisk/view/SafetyNet.java b/app/src/main/java/com/topjohnwu/magisk/view/SafetyNet.java index 72ae3a5bb..e6a8615d1 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/SafetyNet.java +++ b/app/src/main/java/com/topjohnwu/magisk/view/SafetyNet.java @@ -10,9 +10,6 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; -import androidx.annotation.StringRes; -import androidx.cardview.widget.CardView; - import com.topjohnwu.magisk.App; import com.topjohnwu.magisk.Const; import com.topjohnwu.magisk.R; @@ -23,6 +20,8 @@ import com.topjohnwu.superuser.Shell; import java.io.File; +import androidx.annotation.StringRes; +import androidx.cardview.widget.CardView; import butterknife.BindColor; import butterknife.BindView; import butterknife.OnClick; @@ -31,10 +30,10 @@ import dalvik.system.DexClassLoader; public class SafetyNet implements ISafetyNetHelper.Callback { - private static final File EXT_APK = + public static final File EXT_APK = new File(App.self.getFilesDir().getParent() + "/snet", "snet.apk"); - @BindView(R.id.safetyNet_card) CardView safetyNetCard; + /*@BindView(R.id.safetyNet_card) */ CardView safetyNetCard; @BindView(R.id.safetyNet_refresh) ImageView safetyNetRefreshIcon; @BindView(R.id.safetyNet_status) TextView safetyNetStatusText; @BindView(R.id.safetyNet_check_progress) ProgressBar safetyNetProgress; @@ -48,7 +47,7 @@ public class SafetyNet implements ISafetyNetHelper.Callback { @BindColor(R.color.green500) int colorOK; public Unbinder unbinder; - private ExpandableViewHolder expandable; + private final ExpandableViewHolder expandable; public SafetyNet(View v) { unbinder = new SafetyNet_ViewBinding(this, v); @@ -58,27 +57,16 @@ public class SafetyNet implements ISafetyNetHelper.Callback { View.VISIBLE : View.GONE); } - @OnClick(R.id.safetyNet_refresh) - void safetyNet(View v) { - Runnable task = () -> { - safetyNetProgress.setVisibility(View.VISIBLE); - safetyNetRefreshIcon.setVisibility(View.INVISIBLE); - safetyNetStatusText.setText(R.string.checking_safetyNet_status); - check((Activity) v.getContext()); - expandable.collapse(); - }; - if (!SafetyNet.EXT_APK.exists()) { - // Show dialog - new CustomAlertDialog((Activity) v.getContext()) - .setTitle(R.string.proprietary_title) - .setMessage(R.string.proprietary_notice) - .setCancelable(true) - .setPositiveButton(R.string.yes, (d, i) -> task.run()) - .setNegativeButton(R.string.no_thanks, null) - .show(); - } else { - task.run(); - } + public static void dyRun(Activity activity, Object callback) throws Exception { + DexClassLoader loader = new DexClassLoader(EXT_APK.getPath(), EXT_APK.getParent(), + null, ISafetyNetHelper.class.getClassLoader()); + Class clazz = loader.loadClass("com.topjohnwu.snet.Snet"); + ISafetyNetHelper helper = (ISafetyNetHelper) clazz.getMethod("newHelper", + Class.class, String.class, Activity.class, Object.class) + .invoke(null, ISafetyNetHelper.class, EXT_APK.getPath(), activity, callback); + if (helper.getVersion() < Const.SNET_EXT_VER) + throw new Exception(); + helper.attest(); } public void reset() { @@ -120,27 +108,38 @@ public class SafetyNet implements ISafetyNetHelper.Callback { } } - private void dyRun(Activity activity) throws Exception { - DexClassLoader loader = new DexClassLoader(EXT_APK.getPath(), EXT_APK.getParent(), - null, ISafetyNetHelper.class.getClassLoader()); - Class clazz = loader.loadClass("com.topjohnwu.snet.Snet"); - ISafetyNetHelper helper = (ISafetyNetHelper) clazz.getMethod("newHelper", - Class.class, String.class, Activity.class, Object.class) - .invoke(null, ISafetyNetHelper.class, EXT_APK.getPath(), activity, this); - if (helper.getVersion() < Const.SNET_EXT_VER) - throw new Exception(); - helper.attest(); + @OnClick(R.id.safetyNet_refresh) + void safetyNet(View v) { + Runnable task = () -> { + safetyNetProgress.setVisibility(View.VISIBLE); + safetyNetRefreshIcon.setVisibility(View.INVISIBLE); + safetyNetStatusText.setText(R.string.checking_safetyNet_status); + check((Activity) v.getContext()); + expandable.collapse(); + }; + if (!EXT_APK.exists()) { + // Show dialog + new CustomAlertDialog(v.getContext()) + .setTitle(R.string.proprietary_title) + .setMessage(R.string.proprietary_notice) + .setCancelable(true) + .setPositiveButton(R.string.yes, (d, i) -> task.run()) + .setNegativeButton(R.string.no_thanks, null) + .show(); + } else { + task.run(); + } } private void check(Activity activity) { try { - dyRun(activity); + dyRun(activity, this); } catch (Exception ignored) { Shell.sh("rm -rf " + EXT_APK.getParent()).exec(); EXT_APK.getParentFile().mkdir(); Networking.get(Const.Url.SNET_URL).getAsFile(EXT_APK, f -> { try { - dyRun(activity); + dyRun(activity, this); } catch (Exception e) { e.printStackTrace(); onResponse(-1); diff --git a/app/src/main/java/com/topjohnwu/magisk/view/Shortcuts.java b/app/src/main/java/com/topjohnwu/magisk/view/Shortcuts.java index 1cefd9e52..ba841564c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/Shortcuts.java +++ b/app/src/main/java/com/topjohnwu/magisk/view/Shortcuts.java @@ -7,8 +7,6 @@ import android.content.pm.ShortcutManager; import android.graphics.drawable.Icon; import android.os.Build; -import androidx.annotation.RequiresApi; - import com.topjohnwu.magisk.ClassMap; import com.topjohnwu.magisk.Config; import com.topjohnwu.magisk.Const; @@ -19,6 +17,8 @@ import com.topjohnwu.superuser.Shell; import java.util.ArrayList; +import androidx.annotation.RequiresApi; + public class Shortcuts { public static void setup(Context context) { diff --git a/app/src/main/java/com/topjohnwu/magisk/view/SnackbarMaker.java b/app/src/main/java/com/topjohnwu/magisk/view/SnackbarMaker.java index f4c4b2ac4..dc0f1ca61 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/SnackbarMaker.java +++ b/app/src/main/java/com/topjohnwu/magisk/view/SnackbarMaker.java @@ -5,12 +5,12 @@ import android.net.Uri; import android.view.View; import android.widget.TextView; -import androidx.annotation.StringRes; - import com.google.android.material.snackbar.Snackbar; import com.topjohnwu.magisk.R; import com.topjohnwu.magisk.utils.Utils; +import androidx.annotation.StringRes; + public class SnackbarMaker { public static Snackbar make(Activity activity, CharSequence text, int duration) { diff --git a/app/src/main/java/com/topjohnwu/magisk/view/UpdateCardHolder.java b/app/src/main/java/com/topjohnwu/magisk/view/UpdateCardHolder.java deleted file mode 100644 index 3e657e458..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/view/UpdateCardHolder.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.topjohnwu.magisk.view; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; - -import com.topjohnwu.magisk.R; - -import butterknife.BindView; -import butterknife.Unbinder; - -public class UpdateCardHolder { - - @BindView(R.id.status_icon) public ImageView statusIcon; - @BindView(R.id.progress) public ProgressBar progress; - @BindView(R.id.status) public TextView status; - @BindView(R.id.current_version) public TextView currentVersion; - @BindView(R.id.latest_version) public TextView latestVersion; - @BindView(R.id.additional) public TextView additional; - @BindView(R.id.install) public Button install; - - public View itemView; - public Unbinder unbinder; - - public UpdateCardHolder(LayoutInflater inflater, ViewGroup root) { - itemView = inflater.inflate(R.layout.update_card, root, false); - unbinder = new UpdateCardHolder_ViewBinding(this, itemView); - } - - public void setClickable(View.OnClickListener listener) { - itemView.setClickable(true); - itemView.setFocusable(true); - itemView.setOnClickListener(listener); - } - - public void setValid(boolean valid) { - progress.setVisibility(View.GONE); - statusIcon.setVisibility(View.VISIBLE); - if (valid) { - install.setVisibility(View.VISIBLE); - latestVersion.setVisibility(View.VISIBLE); - } else { - install.setVisibility(View.GONE); - latestVersion.setVisibility(View.GONE); - } - } - - public void reset() { - progress.setVisibility(View.VISIBLE); - statusIcon.setVisibility(View.INVISIBLE); - latestVersion.setVisibility(View.GONE); - install.setVisibility(View.GONE); - status.setText(R.string.checking_for_updates); - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/CustomAlertDialog.java b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/CustomAlertDialog.java index 7287b49e3..2425f0e0c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/CustomAlertDialog.java +++ b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/CustomAlertDialog.java @@ -1,6 +1,6 @@ package com.topjohnwu.magisk.view.dialogs; -import android.app.Activity; +import android.content.Context; import android.content.DialogInterface; import android.view.LayoutInflater; import android.view.View; @@ -8,14 +8,13 @@ import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; +import com.topjohnwu.magisk.R; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.StyleRes; import androidx.appcompat.app.AlertDialog; - -import com.topjohnwu.magisk.R; - import butterknife.BindView; public class CustomAlertDialog extends AlertDialog.Builder { @@ -53,11 +52,11 @@ public class CustomAlertDialog extends AlertDialog.Builder { } - public CustomAlertDialog(@NonNull Activity context) { + public CustomAlertDialog(@NonNull Context context) { super(context); } - public CustomAlertDialog(@NonNull Activity context, @StyleRes int themeResId) { + public CustomAlertDialog(@NonNull Context context, @StyleRes int themeResId) { super(context, themeResId); } @@ -89,7 +88,7 @@ public class CustomAlertDialog extends AlertDialog.Builder { vh.positive.setVisibility(View.VISIBLE); vh.positive.setText(text); positiveListener = listener; - vh.positive.setOnClickListener((v) -> { + vh.positive.setOnClickListener(v -> { if (positiveListener != null) { positiveListener.onClick(dialog, DialogInterface.BUTTON_POSITIVE); } @@ -109,7 +108,7 @@ public class CustomAlertDialog extends AlertDialog.Builder { vh.negative.setVisibility(View.VISIBLE); vh.negative.setText(text); negativeListener = listener; - vh.negative.setOnClickListener((v) -> { + vh.negative.setOnClickListener(v -> { if (negativeListener != null) { negativeListener.onClick(dialog, DialogInterface.BUTTON_NEGATIVE); } @@ -129,7 +128,7 @@ public class CustomAlertDialog extends AlertDialog.Builder { vh.neutral.setVisibility(View.VISIBLE); vh.neutral.setText(text); neutralListener = listener; - vh.neutral.setOnClickListener((v) -> { + vh.neutral.setOnClickListener(v -> { if (neutralListener != null) { neutralListener.onClick(dialog, DialogInterface.BUTTON_NEUTRAL); } diff --git a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/EnvFixDialog.java b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/EnvFixDialog.java index 4d974e358..95e1364b6 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/EnvFixDialog.java +++ b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/EnvFixDialog.java @@ -4,8 +4,6 @@ import android.app.Activity; import android.app.ProgressDialog; import android.widget.Toast; -import androidx.annotation.NonNull; - import com.topjohnwu.magisk.R; import com.topjohnwu.magisk.tasks.MagiskInstaller; import com.topjohnwu.magisk.utils.RootUtils; @@ -14,6 +12,8 @@ import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.internal.UiThreadHandler; import com.topjohnwu.superuser.io.SuFile; +import androidx.annotation.NonNull; + public class EnvFixDialog extends CustomAlertDialog { public EnvFixDialog(@NonNull Activity activity) { diff --git a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/FingerprintAuthDialog.java b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/FingerprintAuthDialog.java index 92bb88e97..eaf71b846 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/FingerprintAuthDialog.java +++ b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/FingerprintAuthDialog.java @@ -11,20 +11,23 @@ import android.os.Build; import android.view.Gravity; import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; - import com.topjohnwu.magisk.R; import com.topjohnwu.magisk.utils.FingerprintHelper; import com.topjohnwu.magisk.utils.Utils; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + @TargetApi(Build.VERSION_CODES.M) public class FingerprintAuthDialog extends CustomAlertDialog { - private Runnable callback; + private final Runnable callback; + @Nullable + private Runnable failureCallback; private DialogFingerprintHelper helper; - public FingerprintAuthDialog(@NonNull Activity activity, Runnable onSuccess) { + public FingerprintAuthDialog(@NonNull Activity activity, @NonNull Runnable onSuccess) { super(activity); callback = onSuccess; Drawable fingerprint = activity.getResources().getDrawable(R.drawable.ic_fingerprint); @@ -37,13 +40,28 @@ public class FingerprintAuthDialog extends CustomAlertDialog { vh.messageView.setCompoundDrawablePadding(Utils.dpInPx(20)); vh.messageView.setGravity(Gravity.CENTER); setMessage(R.string.auth_fingerprint); - setNegativeButton(R.string.close, (d, w) -> helper.cancel()); - setOnCancelListener(d -> helper.cancel()); + setNegativeButton(R.string.close, (d, w) -> { + helper.cancel(); + if (failureCallback != null) { + failureCallback.run(); + } + }); + setOnCancelListener(d -> { + helper.cancel(); + if (failureCallback != null) { + failureCallback.run(); + } + }); try { helper = new DialogFingerprintHelper(); } catch (Exception ignored) {} } + public FingerprintAuthDialog(@NonNull Activity activity, @NonNull Runnable onSuccess, @NonNull Runnable onFailure) { + this(activity, onSuccess); + failureCallback = onFailure; + } + @Override public AlertDialog show() { create(); diff --git a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/InstallMethodDialog.java b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/InstallMethodDialog.java index 4ba97f55a..4faea6bd9 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/InstallMethodDialog.java +++ b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/InstallMethodDialog.java @@ -4,14 +4,12 @@ import android.app.Activity; import android.content.Intent; import android.widget.Toast; -import androidx.appcompat.app.AlertDialog; - import com.google.android.material.snackbar.Snackbar; 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.base.IBaseLeanback; import com.topjohnwu.magisk.ui.flash.FlashActivity; import com.topjohnwu.magisk.utils.Utils; import com.topjohnwu.magisk.view.ProgressNotification; @@ -21,9 +19,11 @@ import com.topjohnwu.net.Networking; import java.io.File; import java.util.List; +import androidx.appcompat.app.AlertDialog; + class InstallMethodDialog extends AlertDialog.Builder { - InstallMethodDialog(BaseActivity activity, List options) { + InstallMethodDialog(Ctxt activity, List options) { super(activity); setTitle(R.string.select_method); setItems(options.toArray(new String[0]), (dialog, idx) -> { @@ -48,7 +48,7 @@ class InstallMethodDialog extends AlertDialog.Builder { }); } - private void patchBoot(BaseActivity activity) { + private void patchBoot(Ctxt activity) { activity.runWithExternalRW(() -> { Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG); Intent intent = new Intent(Intent.ACTION_GET_CONTENT) @@ -66,7 +66,7 @@ class InstallMethodDialog extends AlertDialog.Builder { }); } - private void downloadOnly(BaseActivity activity) { + private void downloadOnly(Ctxt activity) { activity.runWithExternalRW(() -> { String filename = Utils.fmt("Magisk-v%s(%d).zip", Config.remoteMagiskVersionString, Config.remoteMagiskVersionCode); @@ -74,7 +74,7 @@ class InstallMethodDialog extends AlertDialog.Builder { ProgressNotification progress = new ProgressNotification(filename); Networking.get(Config.magiskLink) .setDownloadProgressListener(progress) - .setErrorHandler(((conn, e) -> progress.dlFail())) + .setErrorHandler((conn, e) -> progress.dlFail()) .getAsFile(zip, f -> { progress.dlDone(); SnackbarMaker.make(activity, @@ -84,7 +84,7 @@ class InstallMethodDialog extends AlertDialog.Builder { }); } - private void installInactiveSlot(BaseActivity activity) { + private void installInactiveSlot(Ctxt activity) { new CustomAlertDialog(activity) .setTitle(R.string.warning) .setMessage(R.string.install_inactive_slot_msg) diff --git a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/MagiskInstallDialog.java b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/MagiskInstallDialog.java index 64426c656..e1198a92f 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/MagiskInstallDialog.java +++ b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/MagiskInstallDialog.java @@ -1,11 +1,12 @@ package com.topjohnwu.magisk.view.dialogs; +import android.app.Activity; import android.net.Uri; import android.text.TextUtils; import com.topjohnwu.magisk.Config; import com.topjohnwu.magisk.R; -import com.topjohnwu.magisk.ui.base.BaseActivity; +import com.topjohnwu.magisk.ui.base.IBaseLeanback; import com.topjohnwu.magisk.utils.Utils; import com.topjohnwu.magisk.view.MarkDownWindow; import com.topjohnwu.superuser.Shell; @@ -15,7 +16,7 @@ import java.util.ArrayList; import java.util.List; public class MagiskInstallDialog extends CustomAlertDialog { - public MagiskInstallDialog(BaseActivity a) { + public MagiskInstallDialog(Ctxt a) { super(a); String filename = Utils.fmt("Magisk-v%s(%d).zip", Config.remoteMagiskVersionString, Config.remoteMagiskVersionCode); diff --git a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/UninstallDialog.java b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/UninstallDialog.java index eae2dbbd4..4fae38407 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/dialogs/UninstallDialog.java +++ b/app/src/main/java/com/topjohnwu/magisk/view/dialogs/UninstallDialog.java @@ -7,8 +7,6 @@ import android.net.Uri; import android.text.TextUtils; import android.widget.Toast; -import androidx.annotation.NonNull; - import com.topjohnwu.magisk.ClassMap; import com.topjohnwu.magisk.Config; import com.topjohnwu.magisk.Const; @@ -21,6 +19,8 @@ import com.topjohnwu.superuser.Shell; import java.io.File; +import androidx.annotation.NonNull; + public class UninstallDialog extends CustomAlertDialog { public UninstallDialog(@NonNull Activity activity) { diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 000000000..928252fff --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_checked.xml b/app/src/main/res/drawable/ic_checked.xml new file mode 100644 index 000000000..696760bdb --- /dev/null +++ b/app/src/main/res/drawable/ic_checked.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_indeterminate.xml b/app/src/main/res/drawable/ic_indeterminate.xml new file mode 100644 index 000000000..0d821b586 --- /dev/null +++ b/app/src/main/res/drawable/ic_indeterminate.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu.xml b/app/src/main/res/drawable/ic_menu.xml new file mode 100644 index 000000000..d1bac8735 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_restart.xml b/app/src/main/res/drawable/ic_restart.xml new file mode 100644 index 000000000..5d7c2e1f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_restart.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_unchecked.xml b/app/src/main/res/drawable/ic_unchecked.xml new file mode 100644 index 000000000..e90297236 --- /dev/null +++ b/app/src/main/res/drawable/ic_unchecked.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_flash.xml b/app/src/main/res/layout/activity_flash.xml index 0bc141964..194303896 100644 --- a/app/src/main/res/layout/activity_flash.xml +++ b/app/src/main/res/layout/activity_flash.xml @@ -1,61 +1,150 @@ - + - + - + + + + + android:layout_height="match_parent"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - + - + -