From 3376e4e03a3df30c8cf60fad6567f325ac53290f Mon Sep 17 00:00:00 2001 From: Kieron Quinn Date: Thu, 8 Jul 2021 21:38:58 +0100 Subject: [PATCH] 2.1 - Backup and Restore your config to device or the cloud - A rewritten Xposed module with the ability to make even more apps work with force dark, including Snapchat and an option (enabled by default) to fix the inverted (black on dark) status bar icon colors. - Fixed crashes related to navigation in the app - Fixed a crash where requesting root could cause a crash - Improved some Material You theming throughout the app --- .idea/navEditor.xml | 76 ++++++++++ app/build.gradle | 11 +- app/release/output-metadata.json | 4 +- app/src/main/AndroidManifest.xml | 5 +- app/src/main/assets/faq.md | 2 - .../kieronquinn/app/darq/DarqApplication.kt | 12 ++ .../java/com/kieronquinn/app/darq/Xposed.kt | 112 ++++++++++++++- .../settings/AppSharedPreferences.kt | 34 ++++- .../settings/BaseSharedPreferences.kt | 1 + .../settings/DarqSharedPreferences.kt | 49 ++++++- .../settings/XposedSharedPreferences.kt | 134 ++++++++++++++++++ .../app/darq/model/settings/SettingsBackup.kt | 16 +++ .../app/darq/model/settings/SettingsItem.kt | 10 +- .../app/darq/model/xposed/XposedSelfHooks.kt | 13 ++ .../app/darq/model/xposed/XposedSettings.kt | 11 ++ .../darq/ui/base/BaseBottomSheetFragment.kt | 2 +- .../BackupRestoreBottomSheetFragment.kt | 59 ++++++++ .../BackupRestoreBottomSheetViewModel.kt | 70 +++++++++ .../BackupRestoreBackupBottomSheetFragment.kt | 62 ++++++++ ...BackupRestoreBackupBottomSheetViewModel.kt | 102 +++++++++++++ ...BackupRestoreRestoreBottomSheetFragment.kt | 66 +++++++++ ...ackupRestoreRestoreBottomSheetViewModel.kt | 106 ++++++++++++++ .../UpdateDownloadBottomSheetFragment.kt | 9 ++ .../ui/screens/container/ContainerFragment.kt | 5 +- .../container/ContainerSharedViewModel.kt | 12 ++ .../screens/settings/BaseSettingsFragment.kt | 9 +- .../ui/screens/settings/SettingsAdapter.kt | 15 ++ .../ui/screens/settings/SettingsFragment.kt | 38 ++++- .../ui/screens/settings/SettingsViewModel.kt | 14 ++ .../advanced/SettingsAdvancedFragment.kt | 4 +- .../apppicker/SettingsAppPickerFragment.kt | 8 +- .../settings/faq/SettingsFaqFragment.kt | 8 +- .../settings/xposed/XposedSettingsFragment.kt | 58 ++++++++ .../app/darq/ui/utils/TransitionUtils.kt | 6 +- .../utils/extensions/Extensions+Context.kt | 5 +- .../darq/utils/extensions/Extensions+Gzip.kt | 15 ++ .../utils/extensions/Extensions+Navigation.kt | 12 ++ app/src/main/res/drawable/background_menu.xml | 7 + .../button_background_backup_restore.xml | 5 + app/src/main/res/drawable/ic_about_small.xml | 15 ++ app/src/main/res/drawable/ic_backup.xml | 5 + app/src/main/res/drawable/ic_restore.xml | 5 + .../main/res/drawable/ic_restore_large.xml | 5 + .../main/res/drawable/ic_restore_round.xml | 17 +++ app/src/main/res/drawable/ic_xposed.xml | 10 ++ app/src/main/res/drawable/ic_xposed_round.xml | 17 +++ .../drawable/ic_xposed_status_bar_invert.xml | 5 + .../main/res/layout/fragment_app_picker.xml | 1 + .../layout/fragment_bottom_sheet_backup.xml | 35 +++++ .../fragment_bottom_sheet_backup_restore.xml | 102 +++++++++++++ .../layout/fragment_bottom_sheet_restore.xml | 35 +++++ app/src/main/res/layout/fragment_settings.xml | 5 +- .../res/layout/fragment_settings_advanced.xml | 1 + .../fragment_settings_developer_options.xml | 1 + .../main/res/layout/fragment_settings_faq.xml | 1 + .../res/layout/fragment_settings_xposed.xml | 18 +++ app/src/main/res/layout/item_app.xml | 2 + .../main/res/navigation/nav_graph_main.xml | 46 ++++++ app/src/main/res/values/strings.xml | 18 +++ app/src/main/res/values/styles.xml | 13 ++ 60 files changed, 1494 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/com/kieronquinn/app/darq/components/settings/XposedSharedPreferences.kt create mode 100644 app/src/main/java/com/kieronquinn/app/darq/model/settings/SettingsBackup.kt create mode 100644 app/src/main/java/com/kieronquinn/app/darq/model/xposed/XposedSelfHooks.kt create mode 100644 app/src/main/java/com/kieronquinn/app/darq/model/xposed/XposedSettings.kt create mode 100644 app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/BackupRestoreBottomSheetFragment.kt create mode 100644 app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/BackupRestoreBottomSheetViewModel.kt create mode 100644 app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/backup/BackupRestoreBackupBottomSheetFragment.kt create mode 100644 app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/backup/BackupRestoreBackupBottomSheetViewModel.kt create mode 100644 app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/restore/BackupRestoreRestoreBottomSheetFragment.kt create mode 100644 app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/restore/BackupRestoreRestoreBottomSheetViewModel.kt create mode 100644 app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/xposed/XposedSettingsFragment.kt create mode 100644 app/src/main/java/com/kieronquinn/app/darq/utils/extensions/Extensions+Gzip.kt create mode 100644 app/src/main/java/com/kieronquinn/app/darq/utils/extensions/Extensions+Navigation.kt create mode 100644 app/src/main/res/drawable/background_menu.xml create mode 100644 app/src/main/res/drawable/button_background_backup_restore.xml create mode 100644 app/src/main/res/drawable/ic_about_small.xml create mode 100644 app/src/main/res/drawable/ic_backup.xml create mode 100644 app/src/main/res/drawable/ic_restore.xml create mode 100644 app/src/main/res/drawable/ic_restore_large.xml create mode 100644 app/src/main/res/drawable/ic_restore_round.xml create mode 100644 app/src/main/res/drawable/ic_xposed.xml create mode 100644 app/src/main/res/drawable/ic_xposed_round.xml create mode 100644 app/src/main/res/drawable/ic_xposed_status_bar_invert.xml create mode 100644 app/src/main/res/layout/fragment_bottom_sheet_backup.xml create mode 100644 app/src/main/res/layout/fragment_bottom_sheet_backup_restore.xml create mode 100644 app/src/main/res/layout/fragment_bottom_sheet_restore.xml create mode 100644 app/src/main/res/layout/fragment_settings_xposed.xml diff --git a/.idea/navEditor.xml b/.idea/navEditor.xml index f2302c1..c35be0c 100644 --- a/.idea/navEditor.xml +++ b/.idea/navEditor.xml @@ -115,6 +115,51 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -231,6 +276,11 @@ @@ -311,6 +366,15 @@ + @@ -326,6 +390,18 @@ + + + + + + + diff --git a/app/build.gradle b/app/build.gradle index c659ec8..8a14815 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ plugins { id 'com.google.android.gms.oss-licenses-plugin' } -def tagName = '2.0' +def tagName = '2.1' android { compileSdkVersion 'android-S' @@ -14,8 +14,8 @@ android { applicationId "com.kieronquinn.app.darq" minSdkVersion 29 targetSdkVersion 'S' - versionCode 13 - versionName "2.0" + versionCode 21 + versionName "2.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField("String", "TAG_NAME", "\"${tagName}\"") } @@ -77,7 +77,8 @@ dependencies { //AndroidX implementation 'androidx.appcompat:appcompat:1.3.0' - implementation 'androidx.core:core-ktx:1.5.0' + implementation "androidx.fragment:fragment-ktx:1.3.5" + implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "androidx.work:work-runtime-ktx:2.5.0" @@ -93,7 +94,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-service:$lifecycle_version" //Material - implementation 'com.google.android.material:material:1.4.0-rc01' + implementation 'com.google.android.material:material:1.4.0' //Network implementation 'com.squareup.picasso:picasso:2.71828' diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 7fbf957..dadc9f8 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -10,8 +10,8 @@ { "type": "SINGLE", "filters": [], - "versionCode": 13, - "versionName": "2.0", + "versionCode": 21, + "versionName": "2.1", "outputFile": "app-release.apk" } ] diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 949cc4a..1bec110 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -76,7 +76,10 @@ android:value="@string/xposed_desc" /> + android:value="93" /> + diff --git a/app/src/main/assets/faq.md b/app/src/main/assets/faq.md index 23bf73e..dd54c14 100644 --- a/app/src/main/assets/faq.md +++ b/app/src/main/assets/faq.md @@ -23,8 +23,6 @@ Sometimes an app launches too quickly for force dark to be applied. You may have ## Which apps look good when force dark is enabled? During testing; LinkedIn, Facebook and Google Opinion Rewards were found to be usable with force dark enabled. Plenty more will work too, it's up to you to experiment and see what works. -Please note that some of the above require the Xposed module to be enabled for them to work. - ## Can Force Dark be made to work on all apps without Xposed? No. Apps are able to disable force dark in code, so Xposed is the only way to prevent that. diff --git a/app/src/main/java/com/kieronquinn/app/darq/DarqApplication.kt b/app/src/main/java/com/kieronquinn/app/darq/DarqApplication.kt index 807a2ef..01c9e6a 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/DarqApplication.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/DarqApplication.kt @@ -3,6 +3,7 @@ package com.kieronquinn.app.darq import android.app.Application import android.app.DownloadManager import android.content.Context +import android.util.Log import com.kieronquinn.app.darq.components.github.UpdateChecker import com.kieronquinn.app.darq.components.navigation.Navigation import com.kieronquinn.app.darq.components.navigation.NavigationImpl @@ -10,6 +11,12 @@ import com.kieronquinn.app.darq.components.settings.AppSharedPreferences import com.kieronquinn.app.darq.components.settings.DarqSharedPreferences import com.kieronquinn.app.darq.providers.DarqServiceConnectionProvider import com.kieronquinn.app.darq.providers.blur.BlurProvider +import com.kieronquinn.app.darq.ui.screens.bottomsheets.backuprestore.BackupRestoreBottomSheetViewModel +import com.kieronquinn.app.darq.ui.screens.bottomsheets.backuprestore.BackupRestoreBottomSheetViewModelImpl +import com.kieronquinn.app.darq.ui.screens.bottomsheets.backuprestore.backup.BackupRestoreBackupBottomSheetViewModel +import com.kieronquinn.app.darq.ui.screens.bottomsheets.backuprestore.backup.BackupRestoreBackupBottomSheetViewModelImpl +import com.kieronquinn.app.darq.ui.screens.bottomsheets.backuprestore.restore.BackupRestoreRestoreBottomSheetViewModelImpl +import com.kieronquinn.app.darq.ui.screens.bottomsheets.backuprestore.restore.BackupRestoreRestoreViewModel import com.kieronquinn.app.darq.ui.screens.bottomsheets.update.UpdateDownloadBottomSheetViewModel import com.kieronquinn.app.darq.ui.screens.bottomsheets.update.UpdateDownloadBottomSheetViewModelImpl import com.kieronquinn.app.darq.ui.screens.container.ContainerSharedViewModel @@ -52,6 +59,9 @@ class DarqApplication : Application() { viewModel{ SettingsDeveloperOptionsViewModelImpl(get()) } viewModel{ UpdateDownloadBottomSheetViewModelImpl(getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager) } viewModel { LocationPermissionDialogViewModelImpl(get(), get()) } + viewModel { BackupRestoreBottomSheetViewModelImpl(get()) } + viewModel { BackupRestoreBackupBottomSheetViewModelImpl(get(), get(), get()) } + viewModel { BackupRestoreRestoreBottomSheetViewModelImpl(get(), get(), get()) } } private val providersModule = module { @@ -73,6 +83,8 @@ class DarqApplication : Application() { androidContext(this@DarqApplication) modules(serviceModule, appComponentsModule, viewModelsModule, providersModule) } + val applicationThread = Context::class.java.getMethod("getIApplicationThread").invoke(this as Context) + Log.d("DarQA", "ApplicationThread $applicationThread") Sui.init(packageName) setupMonet() } diff --git a/app/src/main/java/com/kieronquinn/app/darq/Xposed.kt b/app/src/main/java/com/kieronquinn/app/darq/Xposed.kt index 7c115ba..849e79d 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/Xposed.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/Xposed.kt @@ -1,20 +1,118 @@ package com.kieronquinn.app.darq +import android.app.Activity +import android.app.AndroidAppHelper +import android.content.Context +import android.content.res.Resources import android.util.Log import android.view.View -import de.robv.android.xposed.IXposedHookLoadPackage -import de.robv.android.xposed.XC_MethodReplacement -import de.robv.android.xposed.XposedHelpers +import com.kieronquinn.app.darq.components.settings.XposedSharedPreferences +import com.kieronquinn.app.darq.model.xposed.XposedSettings +import com.kieronquinn.app.darq.utils.extensions.isDarkTheme +import de.robv.android.xposed.* import de.robv.android.xposed.callbacks.XC_LoadPackage + class Xposed : IXposedHookLoadPackage { + companion object { + private const val TAG = "DarQXposed" + private const val SHARED_PREFS_FILENAME = "${BuildConfig.APPLICATION_ID}_prefs" + } + + private var xposedSettings: XposedSettings? = null + + private val context by lazy { + AndroidAppHelper.currentApplication() as Context + } + + private val isDarkMode: Boolean + get() = Resources.getSystem().configuration.isDarkTheme + override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { - XposedHelpers.findAndHookMethod(View::class.java, "setForceDarkAllowed", Boolean::class.java, object : XC_MethodReplacement() { - override fun replaceHookedMethod(param: MethodHookParam?): Any? { - Log.d("DarQXposed", "Preventing the use of setForceDarkAllowed for ${lpparam.packageName}") - return null + if(lpparam.packageName == BuildConfig.APPLICATION_ID){ + setupSelfHooks(lpparam.classLoader) + } + XposedHelpers.findAndHookMethod(View::class.java, "setForceDarkAllowed", Boolean::class.java, object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + super.beforeHookedMethod(param) + if(xposedSettings == null) { + xposedSettings = getXposedSettings(context) + } + if(xposedSettings?.enabled == true){ + param.args[0] = true + } + } + }) + XposedHelpers.findAndHookMethod(Activity::class.java, "onResume", object: XC_MethodHook(){ + override fun afterHookedMethod(param: MethodHookParam) { + super.afterHookedMethod(param) + if(xposedSettings == null) { + xposedSettings = getXposedSettings(context) + } + if(xposedSettings?.enabled == true && xposedSettings?.invertStatus == true){ + val activity = param.thisObject as? Activity ?: return + if(isDarkMode) { + activity.window.decorView.run { + post { + systemUiVisibility = + systemUiVisibility.and(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()) + } + } + } + } + } + }) + XposedHelpers.findAndHookMethod("android.graphics.HardwareRenderer", lpparam.classLoader, "setForceDark", Boolean::class.java, object: XC_MethodHook(){ + override fun beforeHookedMethod(param: MethodHookParam) { + super.beforeHookedMethod(param) + if(xposedSettings?.enabled == true && xposedSettings?.aggressiveDark == true) { + Log.i(TAG, "Overriding setForceDark to $isDarkMode for ${lpparam.packageName}") + param.args[0] = isDarkMode + } } }) } + + private fun setupSelfHooks(classLoader: ClassLoader){ + XposedHelpers.findAndHookMethod("com.kieronquinn.app.darq.model.xposed.XposedSelfHooks", classLoader, "isXposedModuleEnabled", object: XC_MethodReplacement(){ + override fun replaceHookedMethod(param: MethodHookParam): Any { + param.result = true + return true + } + }) + XposedHelpers.findAndHookMethod("com.kieronquinn.app.darq.model.xposed.XposedSelfHooks", classLoader, "getXSharedPrefsPath", object: XC_MethodReplacement(){ + override fun replaceHookedMethod(param: MethodHookParam): Any { + val path = XSharedPreferences(BuildConfig.APPLICATION_ID, SHARED_PREFS_FILENAME).file.absolutePath + param.result = path + return path + } + }) + } + + private fun getXposedSettings(context: Context): XposedSettings? { + return try { + val xposedPreferences = XposedSharedPreferences(SHARED_PREFS_FILENAME) + val darqEnabled = xposedPreferences.enabled + val appSelected = xposedPreferences.enabledApps.contains(context.packageName) + val alwaysUseForceDark = xposedPreferences.alwaysForceDark + Log.d(TAG, "Enabled apps ${xposedPreferences.enabledApps.joinToString(", ")}") + return if(!darqEnabled || (!appSelected && !alwaysUseForceDark)){ + XposedSettings(enabled = false).apply { + Log.d(TAG, "Got XposedSettings disabled for ${context.packageName}") + } + }else{ + XposedSettings(enabled = true, aggressiveDark = xposedPreferences.xposedAggressiveDark, invertStatus = xposedPreferences.xposedInvertStatus).apply { + Log.d(TAG, "Got XposedSettings enabled for ${context.packageName}") + } + } + }catch (e: Exception){ + //Don't crash the app + if(BuildConfig.DEBUG) { + Log.e(TAG, "Failed to get XposedSettings for ${context.packageName}", e) + } + null + } + } + } diff --git a/app/src/main/java/com/kieronquinn/app/darq/components/settings/AppSharedPreferences.kt b/app/src/main/java/com/kieronquinn/app/darq/components/settings/AppSharedPreferences.kt index 732e5f9..a088333 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/components/settings/AppSharedPreferences.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/components/settings/AppSharedPreferences.kt @@ -3,8 +3,10 @@ package com.kieronquinn.app.darq.components.settings import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences +import android.util.Log import com.kieronquinn.app.darq.BuildConfig import com.kieronquinn.app.darq.model.settings.IPCSetting +import com.kieronquinn.app.darq.model.xposed.XposedSelfHooks import com.kieronquinn.app.darq.utils.extensions.ReadWriteProperty import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -13,6 +15,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import org.json.JSONArray +import java.io.File import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @@ -21,7 +24,20 @@ import kotlin.reflect.KProperty class AppSharedPreferences(context: Context): DarqSharedPreferences() { override val sharedPreferences: SharedPreferences by lazy { - context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_prefs", Context.MODE_PRIVATE) + val prefsFile = "${BuildConfig.APPLICATION_ID}_prefs" + try { + context.getSharedPreferences( + prefsFile, + Context.MODE_WORLD_READABLE + ).also { + makeWorldReadable() + } + }catch (e: SecurityException){ + context.getSharedPreferences( + prefsFile, + Context.MODE_PRIVATE + ) + } } private val _changed = MutableSharedFlow() @@ -89,6 +105,7 @@ class AppSharedPreferences(context: Context): DarqSharedPreferences() { }, { runInBackground { sharedPreferences.edit().putString(key, it.toJSONArray().toString()).commit() + notifyPrefChange(key) } }) @@ -110,6 +127,7 @@ class AppSharedPreferences(context: Context): DarqSharedPreferences() { } fun notifyPrefChange(key: String){ + makeWorldReadable() val ipcSetting = getIPCSettingForKey(key) ?: return GlobalScope.launch(Dispatchers.IO) { _changed.emit(ipcSetting) @@ -130,4 +148,18 @@ class AppSharedPreferences(context: Context): DarqSharedPreferences() { } } + /** + * Make the redirected prefs file world readable ourselves - fixes a bug in Ed/lsposed + * + * This requires the XSharedPreferences file path, which we get via a self hook. It does nothing + * when the Xposed module is not enabled. + */ + private fun makeWorldReadable(){ + XposedSelfHooks.getXSharedPrefsPath().let { + if(it.isNotEmpty()){ + File(it).setReadable(true, false) + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/darq/components/settings/BaseSharedPreferences.kt b/app/src/main/java/com/kieronquinn/app/darq/components/settings/BaseSharedPreferences.kt index 7633355..637d8fb 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/components/settings/BaseSharedPreferences.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/components/settings/BaseSharedPreferences.kt @@ -16,6 +16,7 @@ abstract class BaseSharedPreferences { inline fun > shared(key: String, default: Enum): ReadWriteProperty { return when(this){ is AppSharedPreferences -> sharedEnum(key, default) + is XposedSharedPreferences -> sharedEnum(key, default) else -> throw NotImplementedError("Unknown shared prefs type") } } diff --git a/app/src/main/java/com/kieronquinn/app/darq/components/settings/DarqSharedPreferences.kt b/app/src/main/java/com/kieronquinn/app/darq/components/settings/DarqSharedPreferences.kt index 4851b8b..97b86d1 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/components/settings/DarqSharedPreferences.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/components/settings/DarqSharedPreferences.kt @@ -1,7 +1,10 @@ package com.kieronquinn.app.darq.components.settings import com.kieronquinn.app.darq.model.settings.IPCSetting +import com.kieronquinn.app.darq.model.settings.SettingsBackup +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext import org.json.JSONArray abstract class DarqSharedPreferences: BaseSharedPreferences() { @@ -29,9 +32,15 @@ abstract class DarqSharedPreferences: BaseSharedPreferences() { private const val KEY_DEVELOPER_OPTIONS = "developer_options" private const val DEFAULT_DEVELOPER_OPTIONS = false + private const val KEY_XPOSED_AGGRESSIVE_DARK = "xposed_aggressive_dark" + const val DEFAULT_XPOSED_AGGRESSIVE_DARK = true + + private const val KEY_XPOSED_INVERT_STATUS_BAR = "xposed_invert_status_bar" + const val DEFAULT_XPOSED_INVERT_STATUS_BAR = true + private const val KEY_UI_MONET_COLOR = "monet_color" - private const val KEY_ENABLED_APPS = "enabled_apps" + const val KEY_ENABLED_APPS = "enabled_apps" } @@ -47,6 +56,9 @@ abstract class DarqSharedPreferences: BaseSharedPreferences() { var monetColor by this.shared(KEY_UI_MONET_COLOR, Integer.MAX_VALUE) var enabledApps by this.sharedJSONArray(KEY_ENABLED_APPS) + var xposedAggressiveDark by this.shared(KEY_XPOSED_AGGRESSIVE_DARK, DEFAULT_XPOSED_AGGRESSIVE_DARK) + var xposedInvertStatus by this.shared(KEY_XPOSED_INVERT_STATUS_BAR, DEFAULT_XPOSED_INVERT_STATUS_BAR) + fun getIPCSettingForKey(key: String): IPCSetting? { return when(key){ KEY_ENABLED -> IPCSetting(enabled = enabled) @@ -61,6 +73,41 @@ abstract class DarqSharedPreferences: BaseSharedPreferences() { return IPCSetting(enabled, oxygenForceDark, alwaysForceDark, sendAppCloses) } + fun getSettingsBackup(): SettingsBackup { + return SettingsBackup( + enabled = this.enabled, + autoDarkTheme = this.autoDarkTheme, + useLocation = this.useLocation, + sendAppCloses = this.sendAppCloses, + oxygenForceDark = this.oxygenForceDark, + alwaysForceDark = this.alwaysForceDark, + developerOptions = this.developerOptions, + monetColor = this.monetColor, + xposedAggressiveDark = this.xposedAggressiveDark, + xposedInvertStatus = this.xposedInvertStatus, + enabledApps = this.enabledApps.toList() + ) + } + + /** + * Restores from a given [SettingsBackup] + * @return Whether useLocation is set, and therefore to prompt for location permission + */ + suspend fun fromSettingsBackup(settingsBackup: SettingsBackup): Boolean = withContext(Dispatchers.IO) { + val currentUseLocation = useLocation && autoDarkTheme + enabled = settingsBackup.enabled + autoDarkTheme = settingsBackup.autoDarkTheme + sendAppCloses = settingsBackup.sendAppCloses + oxygenForceDark = settingsBackup.oxygenForceDark + alwaysForceDark = settingsBackup.alwaysForceDark + developerOptions = settingsBackup.developerOptions + monetColor = settingsBackup.monetColor + xposedAggressiveDark = settingsBackup.xposedAggressiveDark + xposedInvertStatus = settingsBackup.xposedInvertStatus + enabledApps = settingsBackup.enabledApps.toTypedArray() + return@withContext (useLocation && autoDarkTheme) && !currentUseLocation + } + internal fun JSONArray.toStringArray(): Array { return ArrayList().apply { for (i in 0 until this@toStringArray.length()) { diff --git a/app/src/main/java/com/kieronquinn/app/darq/components/settings/XposedSharedPreferences.kt b/app/src/main/java/com/kieronquinn/app/darq/components/settings/XposedSharedPreferences.kt new file mode 100644 index 0000000..6b23189 --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/darq/components/settings/XposedSharedPreferences.kt @@ -0,0 +1,134 @@ +package com.kieronquinn.app.darq.components.settings + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import android.util.Log +import com.kieronquinn.app.darq.BuildConfig +import com.kieronquinn.app.darq.model.settings.IPCSetting +import com.kieronquinn.app.darq.utils.extensions.ReadWriteProperty +import de.robv.android.xposed.XSharedPreferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import org.json.JSONArray +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +//These calls are from background threads and need to await changes so commit is required +@SuppressLint("ApplySharedPref") +class XposedSharedPreferences(prefFileName: String): DarqSharedPreferences() { + + override val sharedPreferences: SharedPreferences by lazy { + XSharedPreferences(BuildConfig.APPLICATION_ID, prefFileName) + } + + private val _changed = MutableSharedFlow() + override val changed: Flow = _changed.asSharedFlow() + + override fun shared(key: String, default: String) = ReadWriteProperty({ + sharedPreferences.getString(key, default) ?: default + }, { + runInBackground { + sharedPreferences.edit().putString(key, it).commit() + notifyPrefChange(key) + } + }) + + override fun shared(key: String, default: Int) = ReadWriteProperty({ + sharedPreferences.getInt(key, default) + }, { + runInBackground { + sharedPreferences.edit().putInt(key, it).commit() + notifyPrefChange(key) + } + }) + + override fun shared(key: String, default: Boolean) = ReadWriteProperty({ + sharedPreferences.getBoolean(key, default) + }, { + runInBackground { + sharedPreferences.edit().putBoolean(key, it).commit() + notifyPrefChange(key) + } + }) + + //The following two, while their types are supported by SharedPreferences, are not by the SharedPrefsProvider + + override fun shared(key: String, default: Float) = ReadWriteProperty({ + sharedPreferences.getString(key, default.toString())?.toFloatOrNull() ?: default + }, { + runInBackground { + sharedPreferences.edit().putString(key, it.toString()).commit() + notifyPrefChange(key) + } + }) + + override fun shared(key: String, default: Long) = ReadWriteProperty({ + sharedPreferences.getString(key, default.toString())?.toLongOrNull() ?: default + }, { + runInBackground { + sharedPreferences.edit().putString(key, it.toString()).commit() + notifyPrefChange(key) + } + }) + + override fun shared(key: String, default: Double) = ReadWriteProperty({ + sharedPreferences.getString(key, default.toString())?.toDoubleOrNull() ?: default + }, { + runInBackground { + sharedPreferences.edit().putString(key, it.toString()).commit() + notifyPrefChange(key) + } + }) + + override fun sharedJSONArray(key: String): ReadWriteProperty> = ReadWriteProperty({ + val rawJson = sharedPreferences.getString(key, "[]") ?: "[]" + JSONArray(rawJson).toStringArray() + }, { + runInBackground { + sharedPreferences.edit().putString(key, it.toJSONArray().toString()).commit() + } + }) + + inline fun > sharedEnum(key: String, default: Enum): ReadWriteProperty { + return object: ReadWriteProperty { + + override operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + return java.lang.Enum.valueOf(T::class.java, sharedPreferences.getString(key, default.name)) + } + + override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + GlobalScope.launch(Dispatchers.IO) { + sharedPreferences.edit().putString(key, value.name).commit() + notifyPrefChange(key) + } + } + + } + } + + fun notifyPrefChange(key: String){ + val ipcSetting = getIPCSettingForKey(key) ?: return + GlobalScope.launch(Dispatchers.IO) { + _changed.emit(ipcSetting) + } + } + + private fun runInBackground(method: suspend () -> Unit){ + GlobalScope.launch(Dispatchers.IO) { + method.invoke() + } + } + + private fun Array.toJSONArray(): JSONArray { + return JSONArray().apply { + this@toJSONArray.forEach { + put(it) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/darq/model/settings/SettingsBackup.kt b/app/src/main/java/com/kieronquinn/app/darq/model/settings/SettingsBackup.kt new file mode 100644 index 0000000..e325694 --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/darq/model/settings/SettingsBackup.kt @@ -0,0 +1,16 @@ +package com.kieronquinn.app.darq.model.settings + +data class SettingsBackup( + val enabled: Boolean, + val autoDarkTheme: Boolean, + //useLocation is backed up but not restored automatically - it shows a prompt first + val useLocation: Boolean, + val sendAppCloses: Boolean, + val oxygenForceDark: Boolean, + val alwaysForceDark: Boolean, + val developerOptions: Boolean, + val monetColor: Int, + val xposedAggressiveDark: Boolean, + val xposedInvertStatus: Boolean, + val enabledApps: List +) \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/darq/model/settings/SettingsItem.kt b/app/src/main/java/com/kieronquinn/app/darq/model/settings/SettingsItem.kt index c49f615..8da7c24 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/model/settings/SettingsItem.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/model/settings/SettingsItem.kt @@ -3,27 +3,29 @@ package com.kieronquinn.app.darq.model.settings import androidx.annotation.DrawableRes import kotlin.reflect.KMutableProperty0 -sealed class SettingsItem constructor(open val itemType: SettingsItemType, open var visible: () -> Boolean = {true}) { +sealed class SettingsItem constructor(open val itemType: SettingsItemType, open var centerIconVertically: Boolean = true, open var visible: () -> Boolean = {true}) { data class Header(val title: String, override var visible: () -> Boolean = {true}): SettingsItem(SettingsItemType.HEADER) data class Setting( @DrawableRes val icon: Int, val title: String, - var content: String? = null, + var content: CharSequence? = null, + override var centerIconVertically: Boolean = true, override var visible: () -> Boolean = {true}, val tapAction: (() -> Unit)? = null ): SettingsItem(SettingsItemType.SETTING) data class SwitchSetting( @DrawableRes val icon: Int, val title: String, - var content: String? = null, + var content: CharSequence? = null, val setting: KMutableProperty0, + override var centerIconVertically: Boolean = true, override var visible: () -> Boolean = {true}, val tapAction: ((Boolean) -> Boolean)? = null ): SettingsItem(SettingsItemType.SWITCH_SETTING) data class AboutSetting( @DrawableRes val icon: Int, val title: String, - var content: String? = null, + var content: CharSequence? = null, override var visible: () -> Boolean = {true}, val tripleTapAction: (() -> Unit)? = null ): SettingsItem(SettingsItemType.ABOUT_SETTING) diff --git a/app/src/main/java/com/kieronquinn/app/darq/model/xposed/XposedSelfHooks.kt b/app/src/main/java/com/kieronquinn/app/darq/model/xposed/XposedSelfHooks.kt new file mode 100644 index 0000000..b8d60de --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/darq/model/xposed/XposedSelfHooks.kt @@ -0,0 +1,13 @@ +package com.kieronquinn.app.darq.model.xposed + +object XposedSelfHooks { + + fun isXposedModuleEnabled(): Boolean { + return false + } + + fun getXSharedPrefsPath(): String { + return "" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/darq/model/xposed/XposedSettings.kt b/app/src/main/java/com/kieronquinn/app/darq/model/xposed/XposedSettings.kt new file mode 100644 index 0000000..9174808 --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/darq/model/xposed/XposedSettings.kt @@ -0,0 +1,11 @@ +package com.kieronquinn.app.darq.model.xposed + +data class XposedSettings( + val enabled: Boolean, + val aggressiveDark: Boolean? = null, + val invertStatus: Boolean? = null +){ + override fun toString(): String { + return "XposedSettings enabled $enabled aggressiveDark $aggressiveDark $invertStatus" + } +} diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/base/BaseBottomSheetFragment.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/base/BaseBottomSheetFragment.kt index 3daffb7..4f2db1d 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/ui/base/BaseBottomSheetFragment.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/base/BaseBottomSheetFragment.kt @@ -79,7 +79,7 @@ abstract class BaseBottomSheetFragment(private val inflate: (Lay } dialog.setOnShowListener { (binding.root.parent as View).backgroundTintList = ColorStateList.valueOf(monet.getBackgroundColor(requireContext())) - behavior = BottomSheetBehavior.from(dialog.findViewById(R.id.design_bottom_sheet)!!).apply { + behavior = dialog.behavior.apply { isDraggable = cancelable state = BottomSheetBehavior.STATE_EXPANDED addBottomSheetCallback(bottomSheetCallback) diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/BackupRestoreBottomSheetFragment.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/BackupRestoreBottomSheetFragment.kt new file mode 100644 index 0000000..8ee2373 --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/BackupRestoreBottomSheetFragment.kt @@ -0,0 +1,59 @@ +package com.kieronquinn.app.darq.ui.screens.bottomsheets.backuprestore + +import android.content.res.ColorStateList +import android.os.Bundle +import android.view.View +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import com.kieronquinn.app.darq.R +import com.kieronquinn.app.darq.databinding.FragmentBottomSheetBackupRestoreBinding +import com.kieronquinn.app.darq.ui.base.BaseBottomSheetFragment +import org.koin.androidx.viewmodel.ext.android.viewModel + +class BackupRestoreBottomSheetFragment: BaseBottomSheetFragment(FragmentBottomSheetBackupRestoreBinding::inflate) { + + private val viewModel by viewModel() + + private val backupSelection = registerForActivityResult(ActivityResultContracts.CreateDocument()){ + if(it != null){ + viewModel.onBackupSelected(it) + } + } + + private val restoreSelection = registerForActivityResult(ActivityResultContracts.OpenDocument()){ + if(it != null){ + viewModel.onRestoreSelected(it) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + with(binding){ + backupRestoreCancel.setOnClickListener { + viewModel.onCancelClicked() + } + backupRestoreBackup.setOnClickListener { + viewModel.onBackupClicked(backupSelection) + } + backupRestoreRestore.setOnClickListener { + viewModel.onRestoreClicked(restoreSelection) + } + val accent = ColorStateList.valueOf(monet.getAccentColor(requireContext())) + binding.backupRestoreIcBackup.imageTintList = accent + binding.backupRestoreIcRestore.imageTintList = accent + binding.backupRestoreCancel.setTextColor(accent) + val secondaryBackground = monet.getBackgroundColorSecondary(requireContext()) ?: monet.getBackgroundColor(requireContext()) + binding.backupRestoreRestore.backgroundTintList = ColorStateList.valueOf(secondaryBackground) + binding.backupRestoreBackup.backgroundTintList = ColorStateList.valueOf(secondaryBackground) + ViewCompat.setOnApplyWindowInsetsListener(root){ view, insets -> + val bottomInset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom + val extraPadding = resources.getDimension(R.dimen.padding_8).toInt() + view.updatePadding(bottom = bottomInset + extraPadding) + insets + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/BackupRestoreBottomSheetViewModel.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/BackupRestoreBottomSheetViewModel.kt new file mode 100644 index 0000000..fe9e78f --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/BackupRestoreBottomSheetViewModel.kt @@ -0,0 +1,70 @@ +package com.kieronquinn.app.darq.ui.screens.bottomsheets.backuprestore + +import android.net.Uri +import androidx.activity.result.ActivityResultLauncher +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.kieronquinn.app.darq.components.navigation.Navigation +import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +abstract class BackupRestoreBottomSheetViewModel : ViewModel() { + + abstract fun onBackupClicked(launcher: ActivityResultLauncher) + abstract fun onBackupSelected(uri: Uri) + abstract fun onRestoreClicked(launcher: ActivityResultLauncher>) + abstract fun onRestoreSelected(uri: Uri) + abstract fun onCancelClicked() + +} + +class BackupRestoreBottomSheetViewModelImpl(private val navigation: Navigation) : + BackupRestoreBottomSheetViewModel() { + + companion object { + private const val backupConfigFilename = "darq-config-%s.darqbkp" + } + + override fun onBackupClicked(launcher: ActivityResultLauncher) { + launcher.launch(getBackupFilename()) + } + + override fun onBackupSelected(uri: Uri) { + viewModelScope.launch { + navigation.navigate( + BackupRestoreBottomSheetFragmentDirections.actionBackupRestoreBottomSheetFragmentToBackupRestoreBackupBottomSheetFragment( + uri + ) + ) + } + } + + override fun onRestoreClicked(launcher: ActivityResultLauncher>) { + launcher.launch(arrayOf("*/*")) + } + + override fun onRestoreSelected(uri: Uri) { + viewModelScope.launch { + navigation.navigate( + BackupRestoreBottomSheetFragmentDirections.actionBackupRestoreBottomSheetFragmentToBackupRestoreRestoreBottomSheetFragment( + uri + ) + ) + } + } + + override fun onCancelClicked() { + viewModelScope.launch { + navigation.navigateBack() + } + } + + private fun getBackupFilename(): String { + return String.format( + backupConfigFilename, + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/backup/BackupRestoreBackupBottomSheetFragment.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/backup/BackupRestoreBackupBottomSheetFragment.kt new file mode 100644 index 0000000..96989ee --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/backup/BackupRestoreBackupBottomSheetFragment.kt @@ -0,0 +1,62 @@ +package com.kieronquinn.app.darq.ui.screens.bottomsheets.backuprestore.backup + +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.navArgs +import com.kieronquinn.app.darq.R +import com.kieronquinn.app.darq.databinding.FragmentBottomSheetBackupBinding +import com.kieronquinn.app.darq.ui.base.BaseBottomSheetFragment +import com.kieronquinn.monetcompat.extensions.views.applyMonet +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import org.koin.androidx.viewmodel.ext.android.viewModel + +class BackupRestoreBackupBottomSheetFragment: BaseBottomSheetFragment(FragmentBottomSheetBackupBinding::inflate) { + + private val viewModel by viewModel() + private val arguments by navArgs() + + override val cancelable = false + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + with(binding){ + fragmentBackupProgress.applyMonet() + ViewCompat.setOnApplyWindowInsetsListener(root){ view, insets -> + val bottomInset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom + val extraPadding = resources.getDimension(R.dimen.padding_8).toInt() + view.updatePadding(bottom = bottomInset + extraPadding) + insets + } + } + lifecycleScope.launchWhenResumed { + launch { + viewModel.state.collect { + if (it is BackupRestoreBackupBottomSheetViewModel.State.Complete) { + handleComplete(it.result) + } + } + } + launch { + viewModel.setOutputUri(arguments.uri) + } + } + } + + private fun handleComplete(result: BackupRestoreBackupBottomSheetViewModel.Result){ + when(result){ + BackupRestoreBackupBottomSheetViewModel.Result.SUCCESS -> { + Toast.makeText(requireContext(), R.string.item_backup_restore_backup_success, Toast.LENGTH_LONG).show() + } + BackupRestoreBackupBottomSheetViewModel.Result.FAILED -> { + Toast.makeText(requireContext(), R.string.item_backup_restore_backup_failed, Toast.LENGTH_LONG).show() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/backup/BackupRestoreBackupBottomSheetViewModel.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/backup/BackupRestoreBackupBottomSheetViewModel.kt new file mode 100644 index 0000000..9ef31e4 --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/backup/BackupRestoreBackupBottomSheetViewModel.kt @@ -0,0 +1,102 @@ +package com.kieronquinn.app.darq.ui.screens.bottomsheets.backuprestore.backup + +import android.content.Context +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.gson.Gson +import com.kieronquinn.app.darq.BuildConfig +import com.kieronquinn.app.darq.R +import com.kieronquinn.app.darq.components.navigation.Navigation +import com.kieronquinn.app.darq.components.settings.DarqSharedPreferences +import com.kieronquinn.app.darq.utils.extensions.gzip +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +abstract class BackupRestoreBackupBottomSheetViewModel: ViewModel() { + + abstract val state: Flow + abstract fun setOutputUri(uri: Uri) + + sealed class State { + object Idle: State() + data class Backup(val uri: Uri): State() + data class Complete(val result: Result): State() + } + + enum class Result { + SUCCESS, FAILED + } + +} + +class BackupRestoreBackupBottomSheetViewModelImpl(private val applicationContext: Context, private val settings: DarqSharedPreferences, private val navigation: Navigation): BackupRestoreBackupBottomSheetViewModel() { + + companion object { + //Minimum time to show for so we don't dismiss immediately + private const val MINIMUM_SHOW_TIME = 2000L + } + + private val _state = MutableStateFlow(State.Idle).apply { + viewModelScope.launch { + collect { + val result = handleState(it) + if(result != null){ + emit(result) + } + } + } + } + override val state: Flow = _state + + override fun setOutputUri(uri: Uri) { + viewModelScope.launch { + if(_state.value is State.Idle){ + _state.emit(State.Backup(uri)) + } + } + } + + private suspend fun handleState(state: State): State? { + return when(state){ + is State.Backup -> State.Complete(createBackup(state.uri)) + is State.Complete -> { + navigation.navigateUpTo(R.id.settingsFragment) + null + } + else -> null + } + } + + private suspend fun createBackup(outputUri: Uri): Result { + val startTime = System.currentTimeMillis() + return withContext(Dispatchers.IO){ + runCatching { + val settingsBackup = Gson().toJson(settings.getSettingsBackup()) + val file = DocumentFile.fromSingleUri(applicationContext, outputUri) ?: return@withContext Result.FAILED + if(!file.canWrite()) return@withContext Result.FAILED + val outputStream = applicationContext.contentResolver.openOutputStream(outputUri) ?: return@withContext Result.FAILED + outputStream.use { + it.write(settingsBackup.gzip()) + it.flush() + } + }.onFailure { + if(BuildConfig.DEBUG){ + it.printStackTrace() + } + } + val remainingTime = MINIMUM_SHOW_TIME - (System.currentTimeMillis() - startTime) + if(remainingTime > 0L){ + delay(remainingTime) + } + return@withContext Result.SUCCESS + } + } + +} diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/restore/BackupRestoreRestoreBottomSheetFragment.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/restore/BackupRestoreRestoreBottomSheetFragment.kt new file mode 100644 index 0000000..f5a5cdf --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/restore/BackupRestoreRestoreBottomSheetFragment.kt @@ -0,0 +1,66 @@ +package com.kieronquinn.app.darq.ui.screens.bottomsheets.backuprestore.restore + +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.navArgs +import com.kieronquinn.app.darq.R +import com.kieronquinn.app.darq.databinding.FragmentBottomSheetRestoreBinding +import com.kieronquinn.app.darq.ui.base.BaseBottomSheetFragment +import com.kieronquinn.app.darq.ui.screens.container.ContainerSharedViewModel +import com.kieronquinn.app.darq.utils.extensions.navGraphViewModel +import com.kieronquinn.monetcompat.extensions.views.applyMonet +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import org.koin.androidx.viewmodel.ext.android.viewModel + +class BackupRestoreRestoreBottomSheetFragment: BaseBottomSheetFragment(FragmentBottomSheetRestoreBinding::inflate) { + + private val viewModel by viewModel() + private val arguments by navArgs() + private val sharedViewModel by navGraphViewModel(R.id.nav_graph_main) + + override val cancelable = false + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + with(binding){ + fragmentRestoreProgress.applyMonet() + ViewCompat.setOnApplyWindowInsetsListener(root){ view, insets -> + val bottomInset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom + val extraPadding = resources.getDimension(R.dimen.padding_8).toInt() + view.updatePadding(bottom = bottomInset + extraPadding) + insets + } + } + lifecycleScope.launchWhenResumed { + launch { + viewModel.state.collect { + if (it is BackupRestoreRestoreViewModel.State.Complete) { + handleComplete(it.result) + } + } + } + launch { + viewModel.setOutputUri(arguments.uri) + } + } + } + + private fun handleComplete(result: BackupRestoreRestoreViewModel.Result){ + when(result){ + BackupRestoreRestoreViewModel.Result.SUCCESS -> { + Toast.makeText(requireContext(), R.string.item_backup_restore_restore_success, Toast.LENGTH_LONG).show() + sharedViewModel.onRestoreSuccess() + } + BackupRestoreRestoreViewModel.Result.FAILED -> { + Toast.makeText(requireContext(), R.string.item_backup_restore_restore_failed, Toast.LENGTH_LONG).show() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/restore/BackupRestoreRestoreBottomSheetViewModel.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/restore/BackupRestoreRestoreBottomSheetViewModel.kt new file mode 100644 index 0000000..a8678cb --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/backuprestore/restore/BackupRestoreRestoreBottomSheetViewModel.kt @@ -0,0 +1,106 @@ +package com.kieronquinn.app.darq.ui.screens.bottomsheets.backuprestore.restore + +import android.content.Context +import android.net.Uri +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.gson.Gson +import com.kieronquinn.app.darq.BuildConfig +import com.kieronquinn.app.darq.R +import com.kieronquinn.app.darq.components.navigation.Navigation +import com.kieronquinn.app.darq.components.settings.DarqSharedPreferences +import com.kieronquinn.app.darq.model.settings.SettingsBackup +import com.kieronquinn.app.darq.utils.extensions.ungzip +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +abstract class BackupRestoreRestoreViewModel: ViewModel() { + + abstract val state: Flow + abstract fun setOutputUri(uri: Uri) + + sealed class State { + object Idle: State() + data class Restore(val uri: Uri): State() + data class Complete(val result: Result, val showLocationPrompt: Boolean = false): State() + } + + enum class Result { + SUCCESS, FAILED + } + +} + +class BackupRestoreRestoreBottomSheetViewModelImpl(private val applicationContext: Context, private val settings: DarqSharedPreferences, private val navigation: Navigation): BackupRestoreRestoreViewModel() { + + companion object { + //Minimum time to show for so we don't dismiss immediately + private const val MINIMUM_SHOW_TIME = 2000L + } + + private val _state = MutableStateFlow(State.Idle).apply { + viewModelScope.launch { + collect { + val result = handleState(it) + if(result != null){ + emit(result) + } + } + } + } + override val state: Flow = _state + + override fun setOutputUri(uri: Uri) { + viewModelScope.launch { + if(_state.value is State.Idle){ + _state.emit(State.Restore(uri)) + } + } + } + + private suspend fun handleState(state: State): State? { + return when(state){ + is State.Restore -> restoreBackup(state.uri) + is State.Complete -> { + navigation.navigateUpTo(R.id.settingsFragment) + if(state.showLocationPrompt){ + navigation.navigate(R.id.action_global_locationPermissionDialogFragment) + } + null + } + else -> null + } + } + + private suspend fun restoreBackup(inputUri: Uri): State { + val startTime = System.currentTimeMillis() + return withContext(Dispatchers.IO){ + runCatching { + val fileInput = applicationContext.contentResolver.openInputStream(inputUri) + ?: return@withContext State.Complete(Result.FAILED) + fileInput.use { + val unGzipped = it.readBytes().ungzip() + val settingsBackup = Gson().fromJson(unGzipped, SettingsBackup::class.java) + val showLocationPrompt = settings.fromSettingsBackup(settingsBackup) + val remainingTime = MINIMUM_SHOW_TIME - (System.currentTimeMillis() - startTime) + if (remainingTime > 0L) { + delay(remainingTime) + } + return@withContext State.Complete(Result.SUCCESS, showLocationPrompt) + } + }.onFailure { + if(BuildConfig.DEBUG){ + it.printStackTrace() + } + } + State.Complete(Result.FAILED) + } + } + +} diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/update/UpdateDownloadBottomSheetFragment.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/update/UpdateDownloadBottomSheetFragment.kt index 58b777e..f460683 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/update/UpdateDownloadBottomSheetFragment.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/bottomsheets/update/UpdateDownloadBottomSheetFragment.kt @@ -3,6 +3,9 @@ package com.kieronquinn.app.darq.ui.screens.bottomsheets.update import android.os.Bundle import android.view.View import android.widget.Toast +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.lifecycle.lifecycleScope import com.kieronquinn.app.darq.R import com.kieronquinn.app.darq.databinding.FragmentBottomSheetUpdateDownloadBinding @@ -32,6 +35,12 @@ class UpdateDownloadBottomSheetFragment: BaseBottomSheetFragment + val bottomInset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom + val extraPadding = resources.getDimension(R.dimen.padding_8).toInt() + view.updatePadding(bottom = bottomInset + extraPadding) + insets + } binding.fragmentUpdateDownloadProgress.applyMonet() val accentColor = monet.getAccentColor(requireContext()) binding.fragmentUpdateDownloadCancel.setTextColor(accentColor) diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/container/ContainerFragment.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/container/ContainerFragment.kt index ac12382..8ec3f39 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/container/ContainerFragment.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/container/ContainerFragment.kt @@ -144,8 +144,8 @@ class ContainerFragment: BoundFragment(FragmentContain private fun handleNavigationEvent(navigationEvent: Navigation.NavigationEvent) { when (navigationEvent) { - is Navigation.NavigationEvent.Directions -> navController.navigate(navigationEvent.directions) - is Navigation.NavigationEvent.Id -> navController.navigate(navigationEvent.id) + is Navigation.NavigationEvent.Directions -> navController.navigateSafely(navigationEvent.directions) + is Navigation.NavigationEvent.Id -> navController.navigateSafely(navigationEvent.id) is Navigation.NavigationEvent.Back -> if(!navController.navigateUp()) activity?.finish() is Navigation.NavigationEvent.PopupTo -> navController.popBackStack( navigationEvent.id, @@ -240,6 +240,7 @@ class ContainerFragment: BoundFragment(FragmentContain } private fun getTopFragment(): Fragment? { + if(!navHostFragment.isAdded) return null return navHostFragment.childFragmentManager.fragments.firstOrNull() } diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/container/ContainerSharedViewModel.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/container/ContainerSharedViewModel.kt index 6c1e23a..86c4477 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/container/ContainerSharedViewModel.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/container/ContainerSharedViewModel.kt @@ -13,6 +13,7 @@ import com.kieronquinn.app.darq.providers.DarqServiceConnectionProvider import com.kieronquinn.app.darq.utils.extensions.isDarkTheme import com.kieronquinn.app.darq.utils.extensions.secureSettingIntFlow import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import java.util.* import kotlin.reflect.KSuspendFunction0 @@ -37,6 +38,9 @@ abstract class ContainerSharedViewModel: ViewModel() { abstract fun setAutoDarkThemeEnabled(enabled: Boolean) + abstract val restoreBus: Flow + abstract fun onRestoreSuccess() + sealed class SyncState { object NotSyncing: SyncState() data class Syncing(val syncQueue: Queue): SyncState() @@ -234,4 +238,12 @@ class ContainerSharedViewModelImpl(context: Context, private val serviceProvider override val syncState: Flow = _syncState override val showSnackbar: Flow = syncState.map { it !is SyncState.NotSyncing }.distinctUntilChanged { old, new -> old == new } + private val _restoreBus = Channel() + override val restoreBus = _restoreBus.receiveAsFlow() + override fun onRestoreSuccess() { + viewModelScope.launch { + _restoreBus.send(Unit) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/BaseSettingsFragment.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/BaseSettingsFragment.kt index 86880e9..c2eb6e8 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/BaseSettingsFragment.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/BaseSettingsFragment.kt @@ -5,6 +5,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.doOnLayout import androidx.core.view.updatePadding import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager @@ -27,11 +28,11 @@ abstract class BaseSettingsFragment(inflate: (LayoutInflater, Vi internal val settings by inject() override fun onCreate(savedInstanceState: Bundle?) { - exitTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), true, rootViewId = R.id.root) - enterTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), true, rootViewId = R.id.root) - returnTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), false, rootViewId = R.id.root) - reenterTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), false, rootViewId = R.id.root) super.onCreate(savedInstanceState) + exitTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), true) + enterTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), true) + returnTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), false) + reenterTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), false) } internal fun setupRecyclerView(recyclerView: RecyclerView, settingsAdapter: SettingsAdapter) { diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/SettingsAdapter.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/SettingsAdapter.kt index 04e43e5..a066082 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/SettingsAdapter.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/SettingsAdapter.kt @@ -3,6 +3,7 @@ package com.kieronquinn.app.darq.ui.screens.settings import android.content.Context import android.graphics.Color import android.graphics.drawable.ColorDrawable +import android.view.Gravity import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.isVisible @@ -94,6 +95,9 @@ class SettingsAdapter(context: Context, private var items: List): action.invoke() } } + if(!item.centerIconVertically){ + binding.root.gravity = Gravity.NO_GRAVITY + } } private fun setupTripleTapActionSetting(binding: ItemSettingAboutBinding, item: SettingsItem.AboutSetting) = with(binding) { @@ -160,6 +164,17 @@ class SettingsAdapter(context: Context, private var items: List): root.setOnClickListener { itemSettingSwitchSwitch.toggle() } + if(!item.centerIconVertically){ + binding.root.gravity = Gravity.NO_GRAVITY + } + } + + fun notifySwitchSettings(){ + items.forEachIndexed { index, settingsItem -> + if(settingsItem is SettingsItem.SwitchSetting) { + notifyItemChanged(index) + } + } } sealed class ViewHolder(open val binding: ViewBinding): RecyclerView.ViewHolder(binding.root) { diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/SettingsFragment.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/SettingsFragment.kt index 9337a8f..290788f 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/SettingsFragment.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/SettingsFragment.kt @@ -7,6 +7,7 @@ import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.style.RelativeSizeSpan import android.text.style.TypefaceSpan +import android.util.Log import android.view.* import androidx.annotation.StringRes import androidx.core.content.res.ResourcesCompat @@ -16,6 +17,7 @@ import com.kieronquinn.app.darq.BuildConfig import com.kieronquinn.app.darq.R import com.kieronquinn.app.darq.databinding.FragmentSettingsBinding import com.kieronquinn.app.darq.model.settings.SettingsItem +import com.kieronquinn.app.darq.model.xposed.XposedSelfHooks import com.kieronquinn.app.darq.ui.base.AutoExpandOnRotate import com.kieronquinn.app.darq.ui.base.ProvidesOverflow import com.kieronquinn.monetcompat.extensions.views.applyMonetRecursively @@ -57,6 +59,19 @@ class SettingsFragment : BaseSettingsFragment(FragmentS getString(R.string.item_advanced_options_content), tapAction = viewModel::onAdvancedOptionsClicked ), + SettingsItem.Setting( + R.drawable.ic_xposed_round, + getString(R.string.item_xposed_title), + getString(R.string.item_xposed_content), + visible = { XposedSelfHooks.isXposedModuleEnabled() }, + tapAction = viewModel::onXposedClicked + ), + SettingsItem.Setting( + R.drawable.ic_restore_round, + getString(R.string.item_backup_restore_title), + getString(R.string.item_backup_restore_content), + tapAction = viewModel::onBackupRestoreClicked + ), SettingsItem.Setting( R.drawable.ic_developer_options_round, getString(R.string.item_developer_options_title), @@ -99,15 +114,13 @@ class SettingsFragment : BaseSettingsFragment(FragmentS setupSnackbarPadding(binding.recyclerView) setupAutoDarkTheme() setupDeveloperOptions() + setupRestoreListener() } private fun setupMainSwitch() { binding.switchMain.run { mainSwitchSwitch.typeface = ResourcesCompat.getFont(requireContext(), R.font.google_sans_text_medium) - mainSwitchSwitch.isChecked = settings.enabled - mainSwitchSwitch.setOnCheckedChangeListener { _, isChecked -> - settings.enabled = isChecked - } + updateMainSwitch() } lifecycleScope.launch { sharedViewModel.switchWarning.collect { @@ -125,6 +138,14 @@ class SettingsFragment : BaseSettingsFragment(FragmentS } } + private fun updateMainSwitch() = with(binding.switchMain) { + mainSwitchSwitch.setOnCheckedChangeListener(null) + mainSwitchSwitch.isChecked = settings.enabled + mainSwitchSwitch.setOnCheckedChangeListener { _, isChecked -> + settings.enabled = isChecked + } + } + //Fragment-level cache to prevent flickering private var isAutoDarkChecked: Boolean? = null private fun setupAutoDarkTheme(){ @@ -162,6 +183,15 @@ class SettingsFragment : BaseSettingsFragment(FragmentS } } + private fun setupRestoreListener(){ + lifecycleScope.launchWhenResumed { + sharedViewModel.restoreBus.collect { + adapter.notifySwitchSettings() + updateMainSwitch() + } + } + } + override fun inflateMenu(menuInflater: MenuInflater, menu: Menu) { menuInflater.inflate(R.menu.menu_main, menu) } diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/SettingsViewModel.kt index cf016c6..849db72 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/SettingsViewModel.kt @@ -16,6 +16,8 @@ abstract class SettingsViewModel: ViewModel() { abstract val developerOptionsVisible: Flow abstract fun onAdvancedOptionsClicked() + abstract fun onXposedClicked() + abstract fun onBackupRestoreClicked() abstract fun onAppWhitelistClicked() abstract fun onDeveloperOptionsClicked() abstract fun onAutoDarkThemeCheckedChange(checked: Boolean, sharedViewModel: ContainerSharedViewModel): Boolean @@ -37,6 +39,18 @@ class SettingsViewModelImpl(private val navigation: Navigation, private val sett } } + override fun onBackupRestoreClicked() { + viewModelScope.launch { + navigation.navigate(SettingsFragmentDirections.actionSettingsFragmentToBackupRestoreBottomSheetFragment()) + } + } + + override fun onXposedClicked() { + viewModelScope.launch { + navigation.navigate(SettingsFragmentDirections.actionSettingsFragmentToXposedSettingsFragment()) + } + } + override fun onAppWhitelistClicked() { viewModelScope.launch { navigation.navigate(SettingsFragmentDirections.actionSettingsFragmentToSettingsAppPickerFragment()) diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/advanced/SettingsAdvancedFragment.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/advanced/SettingsAdvancedFragment.kt index a5ac827..e091f30 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/advanced/SettingsAdvancedFragment.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/advanced/SettingsAdvancedFragment.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import com.kieronquinn.app.darq.R import com.kieronquinn.app.darq.databinding.FragmentSettingsAdvancedBinding +import com.kieronquinn.app.darq.databinding.FragmentSettingsXposedBinding import com.kieronquinn.app.darq.model.settings.SettingsItem import com.kieronquinn.app.darq.ui.base.AutoExpandOnRotate import com.kieronquinn.app.darq.ui.base.BackAvailable @@ -13,7 +14,8 @@ import com.kieronquinn.app.darq.ui.screens.settings.BaseSettingsFragment import com.kieronquinn.app.darq.ui.screens.settings.SettingsAdapter import com.kieronquinn.monetcompat.extensions.views.applyMonetRecursively -class SettingsAdvancedFragment : BaseSettingsFragment(FragmentSettingsAdvancedBinding::inflate), AutoExpandOnRotate, BackAvailable { +class SettingsAdvancedFragment : BaseSettingsFragment( + FragmentSettingsAdvancedBinding::inflate), AutoExpandOnRotate, BackAvailable { override val settingsItems by lazy { listOf( diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/apppicker/SettingsAppPickerFragment.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/apppicker/SettingsAppPickerFragment.kt index 598754a..c6d957c 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/apppicker/SettingsAppPickerFragment.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/apppicker/SettingsAppPickerFragment.kt @@ -57,11 +57,11 @@ class SettingsAppPickerFragment : } override fun onCreate(savedInstanceState: Bundle?) { - exitTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), true, rootViewId = R.id.root) - enterTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), true, rootViewId = R.id.root) - returnTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), false, rootViewId = R.id.root) - reenterTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), false, rootViewId = R.id.root) super.onCreate(savedInstanceState) + exitTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), true) + enterTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), true) + returnTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), false) + reenterTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), false) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/faq/SettingsFaqFragment.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/faq/SettingsFaqFragment.kt index d65e4e8..c9a782f 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/faq/SettingsFaqFragment.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/faq/SettingsFaqFragment.kt @@ -24,10 +24,10 @@ import ru.noties.markwon.core.MarkwonTheme class SettingsFaqFragment: BoundFragment(FragmentSettingsFaqBinding::inflate), BackAvailable, AutoExpandOnRotate { override fun onCreate(savedInstanceState: Bundle?) { - exitTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), true, rootViewId = R.id.root) - enterTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), true, rootViewId = R.id.root) - returnTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), false, rootViewId = R.id.root) - reenterTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), false, rootViewId = R.id.root) + exitTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), true) + enterTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), true) + returnTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), false) + reenterTransition = TransitionUtils.getMaterialSharedAxis(requireContext(), false) super.onCreate(savedInstanceState) } diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/xposed/XposedSettingsFragment.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/xposed/XposedSettingsFragment.kt new file mode 100644 index 0000000..7fdc89a --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/screens/settings/xposed/XposedSettingsFragment.kt @@ -0,0 +1,58 @@ +package com.kieronquinn.app.darq.ui.screens.settings.xposed + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.kieronquinn.app.darq.R +import com.kieronquinn.app.darq.databinding.FragmentSettingsXposedBinding +import com.kieronquinn.app.darq.model.settings.SettingsItem +import com.kieronquinn.app.darq.ui.base.AutoExpandOnRotate +import com.kieronquinn.app.darq.ui.base.BackAvailable +import com.kieronquinn.app.darq.ui.screens.settings.BaseSettingsFragment +import com.kieronquinn.app.darq.ui.screens.settings.SettingsAdapter +import com.kieronquinn.monetcompat.extensions.views.applyMonetRecursively + +class XposedSettingsFragment: BaseSettingsFragment( + FragmentSettingsXposedBinding::inflate), AutoExpandOnRotate, BackAvailable { + + override val settingsItems by lazy { + listOf( + SettingsItem.SwitchSetting( + R.drawable.ic_advanced_always_use_force_dark, + getString(R.string.item_xposed_aggressive_dark_title), + getString(R.string.item_xposed_aggressive_dark_content), + settings::xposedAggressiveDark + ), + SettingsItem.SwitchSetting( + R.drawable.ic_xposed_status_bar_invert, + getString(R.string.item_xposed_invert_status_bar_fix_title), + getString(R.string.item_xposed_invert_status_bar_fix_content), + settings::xposedInvertStatus + ), + SettingsItem.Setting( + R.drawable.ic_about_small, + getString(R.string.item_xposed_info_title), + getText(R.string.item_xposed_info_content), + centerIconVertically = false + ) + ).toMutableList() + } + + private val adapter by lazy { + SettingsAdapter(requireContext(), settingsItems) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = super.onCreateView(inflater, container, savedInstanceState) + view?.applyMonetRecursively() + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView(binding.recyclerView, adapter) + setupSnackbarPadding(binding.recyclerView) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/darq/ui/utils/TransitionUtils.kt b/app/src/main/java/com/kieronquinn/app/darq/ui/utils/TransitionUtils.kt index 802dc0d..3cd596e 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/ui/utils/TransitionUtils.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/ui/utils/TransitionUtils.kt @@ -2,6 +2,7 @@ package com.kieronquinn.app.darq.ui.utils import android.content.Context import android.view.animation.AnimationUtils +import androidx.core.transition.doOnEnd import com.google.android.material.transition.platform.FadeThroughProvider import com.google.android.material.transition.platform.MaterialSharedAxis import com.google.android.material.transition.platform.SlideDistanceProvider @@ -10,11 +11,8 @@ import kotlin.math.roundToInt object TransitionUtils { - fun getMaterialSharedAxis(context: Context, forward: Boolean, rootViewId: Int? = null): MaterialSharedAxis { + fun getMaterialSharedAxis(context: Context, forward: Boolean): MaterialSharedAxis { return MaterialSharedAxis(MaterialSharedAxis.X, forward).apply { - rootViewId?.let { - addTarget(it) - } (primaryAnimatorProvider as SlideDistanceProvider).slideDistance = context.resources.getDimension(R.dimen.shared_axis_x_slide_distance).roundToInt() duration = 450L diff --git a/app/src/main/java/com/kieronquinn/app/darq/utils/extensions/Extensions+Context.kt b/app/src/main/java/com/kieronquinn/app/darq/utils/extensions/Extensions+Context.kt index 49631ae..a57f0c6 100644 --- a/app/src/main/java/com/kieronquinn/app/darq/utils/extensions/Extensions+Context.kt +++ b/app/src/main/java/com/kieronquinn/app/darq/utils/extensions/Extensions+Context.kt @@ -9,8 +9,11 @@ import androidx.annotation.ColorInt import androidx.core.content.ContextCompat val Context.isDarkTheme: Boolean + get() = resources.configuration.isDarkTheme + +val Configuration.isDarkTheme: Boolean get() { - return resources.configuration.uiMode and + return uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES } diff --git a/app/src/main/java/com/kieronquinn/app/darq/utils/extensions/Extensions+Gzip.kt b/app/src/main/java/com/kieronquinn/app/darq/utils/extensions/Extensions+Gzip.kt new file mode 100644 index 0000000..433fe39 --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/darq/utils/extensions/Extensions+Gzip.kt @@ -0,0 +1,15 @@ +package com.kieronquinn.app.darq.utils.extensions + +import java.io.ByteArrayOutputStream +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream + +fun String.gzip(): ByteArray { + return ByteArrayOutputStream().apply { + GZIPOutputStream(this).bufferedWriter().use { it.write(this@gzip) } + }.toByteArray() +} + +fun ByteArray.ungzip(): String { + return GZIPInputStream(this.inputStream()).bufferedReader().use { it.readText() } +} \ No newline at end of file diff --git a/app/src/main/java/com/kieronquinn/app/darq/utils/extensions/Extensions+Navigation.kt b/app/src/main/java/com/kieronquinn/app/darq/utils/extensions/Extensions+Navigation.kt new file mode 100644 index 0000000..94bf045 --- /dev/null +++ b/app/src/main/java/com/kieronquinn/app/darq/utils/extensions/Extensions+Navigation.kt @@ -0,0 +1,12 @@ +package com.kieronquinn.app.darq.utils.extensions + +import androidx.navigation.NavController +import androidx.navigation.NavDirections + +fun NavController.navigateSafely(directions: NavDirections){ + currentDestination?.getAction(directions.actionId)?.let { navigate(directions) } +} + +fun NavController.navigateSafely(actionId: Int){ + currentDestination?.getAction(actionId)?.let { navigate(actionId) } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/background_menu.xml b/app/src/main/res/drawable/background_menu.xml new file mode 100644 index 0000000..eb38ab0 --- /dev/null +++ b/app/src/main/res/drawable/background_menu.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_background_backup_restore.xml b/app/src/main/res/drawable/button_background_backup_restore.xml new file mode 100644 index 0000000..3e2b55e --- /dev/null +++ b/app/src/main/res/drawable/button_background_backup_restore.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_about_small.xml b/app/src/main/res/drawable/ic_about_small.xml new file mode 100644 index 0000000..dccf1db --- /dev/null +++ b/app/src/main/res/drawable/ic_about_small.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_backup.xml b/app/src/main/res/drawable/ic_backup.xml new file mode 100644 index 0000000..967c259 --- /dev/null +++ b/app/src/main/res/drawable/ic_backup.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_restore.xml b/app/src/main/res/drawable/ic_restore.xml new file mode 100644 index 0000000..696181c --- /dev/null +++ b/app/src/main/res/drawable/ic_restore.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_restore_large.xml b/app/src/main/res/drawable/ic_restore_large.xml new file mode 100644 index 0000000..ff92a5e --- /dev/null +++ b/app/src/main/res/drawable/ic_restore_large.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_restore_round.xml b/app/src/main/res/drawable/ic_restore_round.xml new file mode 100644 index 0000000..380c5e8 --- /dev/null +++ b/app/src/main/res/drawable/ic_restore_round.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_xposed.xml b/app/src/main/res/drawable/ic_xposed.xml new file mode 100644 index 0000000..3b4a338 --- /dev/null +++ b/app/src/main/res/drawable/ic_xposed.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_xposed_round.xml b/app/src/main/res/drawable/ic_xposed_round.xml new file mode 100644 index 0000000..e89fac9 --- /dev/null +++ b/app/src/main/res/drawable/ic_xposed_round.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_xposed_status_bar_invert.xml b/app/src/main/res/drawable/ic_xposed_status_bar_invert.xml new file mode 100644 index 0000000..148837f --- /dev/null +++ b/app/src/main/res/drawable/ic_xposed_status_bar_invert.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/fragment_app_picker.xml b/app/src/main/res/layout/fragment_app_picker.xml index a4bd1a9..84c31f0 100644 --- a/app/src/main/res/layout/fragment_app_picker.xml +++ b/app/src/main/res/layout/fragment_app_picker.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:transitionGroup="true" android:id="@+id/root"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bottom_sheet_backup_restore.xml b/app/src/main/res/layout/fragment_bottom_sheet_backup_restore.xml new file mode 100644 index 0000000..fc1a2b2 --- /dev/null +++ b/app/src/main/res/layout/fragment_bottom_sheet_backup_restore.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bottom_sheet_restore.xml b/app/src/main/res/layout/fragment_bottom_sheet_restore.xml new file mode 100644 index 0000000..7b0405e --- /dev/null +++ b/app/src/main/res/layout/fragment_bottom_sheet_restore.xml @@ -0,0 +1,35 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 3824ead..0b632f9 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -1,8 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_app.xml b/app/src/main/res/layout/item_app.xml index 0f56dc4..800162a 100644 --- a/app/src/main/res/layout/item_app.xml +++ b/app/src/main/res/layout/item_app.xml @@ -23,6 +23,8 @@ android:layout_weight="1" android:layout_gravity="center_vertical" tools:text="App Name" + android:maxLines="2" + android:ellipsize="end" android:textColor="?android:textColorPrimary" android:layout_marginStart="@dimen/padding_8" android:layout_marginEnd="@dimen/padding_8" diff --git a/app/src/main/res/navigation/nav_graph_main.xml b/app/src/main/res/navigation/nav_graph_main.xml index 584134e..85c4ff0 100644 --- a/app/src/main/res/navigation/nav_graph_main.xml +++ b/app/src/main/res/navigation/nav_graph_main.xml @@ -34,6 +34,12 @@ app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" /> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e00970a..45ee19d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,24 @@ Service Info Not Connected Connected, service type: %1s + Backup & Restore + Save and restore your DarQ config to device or the cloud + Backup + Creating Backup… + Backup created + Backup failed + Restored successfully + Restore failed + Restoring Backup… + Restore + Xposed Module + Change settings of the DarQ Xposed module + Aggressive Force Dark + Override apps changing theme to a light style when in dark mode. This may cause color issues in some apps. + Fix Status Bar Inversion + Attempt to disable status bar inversion in force dark apps, fixing dark icons on dark background + Xposed Notes + If you are using EdXposed in app whitelist mode, or are using lsposed, you must also enable all the apps you want to apply to in your Xposed manager, as well as in DarQ.\nApps must be restarted after settings changes for them to apply.\nIf a setting or newly selected force dark app is not applying, try rebooting, launching DarQ then launching your selected app.\nWarning: If you uninstall Xposed or enable/disable DarQ\'s module, your DarQ settings may be reset due to Xposed\'s internal settings hooks. Use the backup/restore feature to save your settings before doing this. Monet Color Picker unavailable for the currently selected wallpaper Connecting to service… Syncing changes with service… diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ebdf69f..df2c9aa 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -11,6 +11,7 @@ @android:color/transparent @color/search_box_background @style/AppTheme.TextAppearance.Popup + @style/DarqOverflowMenu + + + +