From c6d7066e91db4ace8c662ceade78398e78a2460f Mon Sep 17 00:00:00 2001 From: Hepolise Date: Sun, 28 Sep 2025 18:42:52 +0300 Subject: [PATCH 01/10] refactor: replace broadcast receiver with content provider for hook status communication - Remove HookBroadcastReceiver and its broadcast-based communication - Add HookStatusProvider as a content provider for secure cross-process communication - Add HookNotifier utility class to handle content provider interactions - Update Constants to reflect new method names (SET_HOOKED, INCREMENT_LAUNCH_COUNT) - Update AndroidManifest.xml to register the content provider instead of broadcast receiver - Update VolumeKeyControlModuleHandlers to use HookNotifier instead of direct broadcasts --- app/src/main/AndroidManifest.xml | 12 ++-- .../module/VolumeKeyControlModuleHandlers.kt | 28 +--------- .../module/util/HookNotifier.kt | 24 ++++++++ .../provider/HookStatusProvider.kt | 56 +++++++++++++++++++ .../receiver/HookBroadcastReceiver.kt | 30 ---------- .../volumekeytrackcontrol/util/Constants.kt | 7 +-- 6 files changed, 90 insertions(+), 67 deletions(-) create mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/HookNotifier.kt create mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/provider/HookStatusProvider.kt delete mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/HookBroadcastReceiver.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9f63baa..500248b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,13 +27,11 @@ - - - - - + Unit) { - val modulePackage = BuildConfig.APPLICATION_ID - - val intent = Intent().apply { - component = ComponentName( - modulePackage, - HookBroadcastReceiver::class.java.name - ) - action = Constants.HOOK_UPDATE - block() - setPackage(modulePackage) - } - sendBroadcast(intent) - } - private fun MethodHookParam.delay(event: MediaEvent) { val handler = getHandler() handler.postDelayed( @@ -311,9 +291,7 @@ object VolumeKeyControlModuleHandlers { log("Sending ${this::class.simpleName}") isLongPress = true sendMediaButtonEventAndTriggerVibration(this) - context.sendBroadcast { - putExtra(Constants.INCREMENT_LAUNCH_COUNT, true) - } + HookNotifier.incrementLaunchCount(context) } object PlayPause : MediaEvent("mVolumeBothLongPress") { diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/HookNotifier.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/HookNotifier.kt new file mode 100644 index 0000000..aab1288 --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/HookNotifier.kt @@ -0,0 +1,24 @@ +package ru.hepolise.volumekeytrackcontrol.module.util + +import android.content.Context +import androidx.core.net.toUri +import ru.hepolise.volumekeytrackcontrol.util.Constants +import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig + +object HookNotifier { + + private val authorityUri = "content://${BuildConfig.APPLICATION_ID}.hookstatusprovider".toUri() + + fun notifyHooked(context: Context) { + runCatching { + context.contentResolver.call(authorityUri, Constants.SET_HOOKED, null, null) + } + } + + fun incrementLaunchCount(context: Context) { + runCatching { + context.contentResolver.call(authorityUri, Constants.INCREMENT_LAUNCH_COUNT, null, null) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/provider/HookStatusProvider.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/provider/HookStatusProvider.kt new file mode 100644 index 0000000..aba75ff --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/provider/HookStatusProvider.kt @@ -0,0 +1,56 @@ +package ru.hepolise.volumekeytrackcontrol.provider + +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Bundle +import androidx.core.content.edit +import ru.hepolise.volumekeytrackcontrol.util.Constants +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.HOOK_PREFS_NAME +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LAST_INIT_HOOK_TIME +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LAUNCHED_COUNT +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLaunchedCount + +class HookStatusProvider : ContentProvider() { + + override fun onCreate(): Boolean = true + + override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { + val context = context ?: return null + val prefs = context.getSharedPreferences(HOOK_PREFS_NAME, Context.MODE_PRIVATE) + + prefs.edit { + when (method) { + Constants.SET_HOOKED -> { + putLong(LAST_INIT_HOOK_TIME, System.currentTimeMillis()) + } + + Constants.INCREMENT_LAUNCH_COUNT -> { + val current = prefs.getLaunchedCount() + putInt(LAUNCHED_COUNT, current + 1) + } + } + } + return null + } + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? = null + + override fun getType(uri: Uri): String? = null + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int = 0 +} diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/HookBroadcastReceiver.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/HookBroadcastReceiver.kt deleted file mode 100644 index 3ee9e2f..0000000 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/HookBroadcastReceiver.kt +++ /dev/null @@ -1,30 +0,0 @@ -package ru.hepolise.volumekeytrackcontrol.receiver - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import androidx.core.content.edit -import ru.hepolise.volumekeytrackcontrol.util.Constants -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.HOOK_PREFS_NAME -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LAST_INIT_HOOK_TIME -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LAUNCHED_COUNT -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLaunchedCount - -class HookBroadcastReceiver : BroadcastReceiver() { - - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == Constants.HOOK_UPDATE) { - val prefs = context.getSharedPreferences(HOOK_PREFS_NAME, Context.MODE_PRIVATE) - prefs.edit { - if (intent.getBooleanExtra(Constants.HOOKED, false)) { - putLong(LAST_INIT_HOOK_TIME, System.currentTimeMillis()) - } - if (intent.getBooleanExtra(Constants.INCREMENT_LAUNCH_COUNT, false)) { - val current = prefs.getLaunchedCount() - putInt(LAUNCHED_COUNT, current + 1) - } - } - } - } -} - diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/Constants.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/Constants.kt index 7079956..29d73b5 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/Constants.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/Constants.kt @@ -1,7 +1,5 @@ package ru.hepolise.volumekeytrackcontrol.util -import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig - object Constants { const val GITHUB_URL = "https://github.com/Hepolise/VolumeKeyTrackControlModule" const val GITHUB_NEW_ISSUE_URL = @@ -9,7 +7,6 @@ object Constants { const val LSPOSED_GITHUB_URL = "https://github.com/JingMatrix/LSPosed/actions/workflows/core.yml" - const val HOOK_UPDATE = BuildConfig.APPLICATION_ID + ".HOOK_UPDATE" - const val HOOKED = "hooked" - const val INCREMENT_LAUNCH_COUNT = "incrementLaunchCount" + const val SET_HOOKED = "set_hooked" + const val INCREMENT_LAUNCH_COUNT = "increment_launch_count" } \ No newline at end of file From 688f6f676558da4829149c925a14d3518ba908f1 Mon Sep 17 00:00:00 2001 From: Hepolise Date: Tue, 30 Sep 2025 21:11:22 +0300 Subject: [PATCH 02/10] fix(AndroidManifest): add directBootAware to provider --- app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 500248b..29fd469 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,6 +30,7 @@ From 204cc05140825eeeff753adf890aefdcdab6af96 Mon Sep 17 00:00:00 2001 From: Hepolise Date: Tue, 30 Sep 2025 21:28:28 +0300 Subject: [PATCH 03/10] fix(ModuleStatus): add ACTION_BOOT_COMPLETED for IntentFilter --- .../module/VolumeKeyControlModuleHandlers.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt index b63007c..84b992e 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt @@ -79,9 +79,13 @@ object VolumeKeyControlModuleHandlers { XposedHelpers.setAdditionalInstanceField(param.thisObject, event.field, runnable) } - val filter = IntentFilter(Intent.ACTION_USER_UNLOCKED) + val filter = IntentFilter().apply { + addAction(Intent.ACTION_BOOT_COMPLETED) + addAction(Intent.ACTION_USER_UNLOCKED) + } context.registerReceiver(object : BroadcastReceiver() { override fun onReceive(ctx: Context, intent: Intent) { + log("onReceive: ${intent.action}") HookNotifier.notifyHooked(context) ctx.unregisterReceiver(this) } From c223db88372b69d26bf994a7914a1b842d589784 Mon Sep 17 00:00:00 2001 From: Hepolise Date: Sun, 19 Oct 2025 15:29:42 +0300 Subject: [PATCH 04/10] feat: Improve module status detection and boot handling - Replace ContentProvider-based hook status tracking with system properties and remote preferences for more reliable detection - Add BootReceiver to handle BOOT_COMPLETED events and track boot state - Implement BootViewModel and BootRepository to manage boot completion state - Add StatusSysPropsHelper for system property-based status tracking - Use RemotePreferences for cross-process preference access - Refactor package structure: move viewmodels to dedicated package - Add loading states and better error handling in SettingsActivity - Add LSPosedLogger for better debugging - Update dependencies to include RemotePreferences library Key changes: - Module status now persists across reboots using system properties - More reliable detection of whether module is active - Better handling of boot completion events - Improved user experience with loading states - Reduced dependency on ContentProvider which was less reliable --- app/build.gradle.kts | 3 + app/src/main/AndroidManifest.xml | 14 ++- .../module/VolumeControlModule.kt | 40 +++--- .../module/VolumeKeyControlModuleHandlers.kt | 35 +++--- .../module/util/HookNotifier.kt | 24 ---- .../module/util/RemotePrefsHelper.kt | 26 ++++ .../module/util/StatusHelper.kt | 68 ++++++++++ .../provider/HookStatusProvider.kt | 56 --------- .../provider/RemotePrefProvider.kt | 16 +++ .../receiver/BootReceiver.kt | 21 ++++ .../repository/BootRepository.kt | 59 +++++++++ .../ui/SettingsActivity.kt | 39 +++--- .../hepolise/volumekeytrackcontrol/ui/Util.kt | 4 +- .../ui/component/AppFilterSetting.kt | 2 +- .../ui/navigation/AppNavigation.kt | 4 +- .../ui/screen/AppFilterScreen.kt | 18 +-- .../ui/screen/SettingsScreen.kt | 119 +++++++++++++----- .../util/AppFilterType.kt | 17 +++ .../util/LSPosedLogger.kt | 10 ++ .../util/SharedPreferencesUtil.kt | 45 ++++--- .../util/StatusSysPropsHelper.kt | 44 +++++++ .../volumekeytrackcontrol/util/SystemProps.kt | 30 +++++ .../{ui => }/viewmodel/AppFilterViewModel.kt | 2 +- .../{ui => }/viewmodel/AppIconViewModel.kt | 2 +- .../viewmodel/BootViewModel.kt | 56 +++++++++ .../viewmodel/BootViewModelFactory.kt | 17 +++ 26 files changed, 568 insertions(+), 203 deletions(-) delete mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/HookNotifier.kt create mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/RemotePrefsHelper.kt create mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/StatusHelper.kt delete mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/provider/HookStatusProvider.kt create mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/provider/RemotePrefProvider.kt create mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/BootReceiver.kt create mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/repository/BootRepository.kt create mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/AppFilterType.kt create mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/LSPosedLogger.kt create mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/StatusSysPropsHelper.kt create mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SystemProps.kt rename app/src/main/java/ru/hepolise/volumekeytrackcontrol/{ui => }/viewmodel/AppFilterViewModel.kt (96%) rename app/src/main/java/ru/hepolise/volumekeytrackcontrol/{ui => }/viewmodel/AppIconViewModel.kt (95%) create mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/BootViewModel.kt create mode 100644 app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/BootViewModelFactory.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3af7f2f..4bd3abc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,4 +67,7 @@ dependencies { // Xposed Framework API dependencies compileOnly("de.robv.android.xposed:api:82") + + // RemotePreferences + implementation("com.crossbowffs.remotepreferences:remotepreferences:0.8") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 29fd469..dc8fccb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + + + + + + + classLoader.init() + } } - init(lpparam.classLoader) } - private fun init(classLoader: ClassLoader) { + private fun ClassLoader.init() { initMethodSignatures.any { (params, logMessage) -> - tryHookInitMethod(classLoader, params, logMessage) + tryHookInitMethod(params, logMessage) }.also { hooked -> if (!hooked) { log("Method hook failed for init!") @@ -70,25 +71,30 @@ class VolumeControlModule : IXposedHookLoadPackage { } } - // https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-14.0.0_r18/services/core/java/com/android/server/policy/PhoneWindowManager.java#4117 - XposedHelpers.findAndHookMethod( - CLASS_PHONE_WINDOW_MANAGER, - classLoader, - "interceptKeyBeforeQueueing", - KeyEvent::class.java, - Int::class.javaPrimitiveType, - handleInterceptKeyBeforeQueueing - ) + try { + // https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-14.0.0_r18/services/core/java/com/android/server/policy/PhoneWindowManager.java#4117 + XposedHelpers.findAndHookMethod( + CLASS_PHONE_WINDOW_MANAGER, + this, + "interceptKeyBeforeQueueing", + KeyEvent::class.java, + Int::class.javaPrimitiveType, + handleInterceptKeyBeforeQueueing + ) + } catch (t: Throwable) { + log("Method hook failed for interceptKeyBeforeQueueing!") + t.message?.let { log(it) } + } + } - private fun tryHookInitMethod( - classLoader: ClassLoader, + private fun ClassLoader.tryHookInitMethod( params: Array, logMessage: String ): Boolean { return try { XposedHelpers.findAndHookMethod( - CLASS_PHONE_WINDOW_MANAGER, classLoader, "init", + CLASS_PHONE_WINDOW_MANAGER, this, "init", *params, handleConstructPhoneWindowManager ) log(logMessage) diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt index 84b992e..d066a3b 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt @@ -1,9 +1,6 @@ package ru.hepolise.volumekeytrackcontrol.module -import android.content.BroadcastReceiver import android.content.Context -import android.content.Intent -import android.content.IntentFilter import android.content.SharedPreferences import android.hardware.display.DisplayManager import android.media.AudioManager @@ -14,14 +11,19 @@ import android.os.PowerManager import android.os.Vibrator import android.view.Display import android.view.KeyEvent +import androidx.core.content.edit import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XC_MethodHook.MethodHookParam import de.robv.android.xposed.XposedHelpers -import ru.hepolise.volumekeytrackcontrol.module.util.HookNotifier import ru.hepolise.volumekeytrackcontrol.module.util.LogHelper +import ru.hepolise.volumekeytrackcontrol.module.util.RemotePrefsHelper +import ru.hepolise.volumekeytrackcontrol.module.util.StatusHelper +import ru.hepolise.volumekeytrackcontrol.util.AppFilterType import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LAUNCHED_COUNT import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getAppFilterType import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getApps +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLaunchedCount import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLongPressDuration import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.isSwapButtons import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.getVibrator @@ -79,17 +81,7 @@ object VolumeKeyControlModuleHandlers { XposedHelpers.setAdditionalInstanceField(param.thisObject, event.field, runnable) } - val filter = IntentFilter().apply { - addAction(Intent.ACTION_BOOT_COMPLETED) - addAction(Intent.ACTION_USER_UNLOCKED) - } - context.registerReceiver(object : BroadcastReceiver() { - override fun onReceive(ctx: Context, intent: Intent) { - log("onReceive: ${intent.action}") - HookNotifier.notifyHooked(context) - ctx.unregisterReceiver(this) - } - }, filter) + StatusHelper.handleSuccessHook(context) } } @@ -223,9 +215,9 @@ object VolumeKeyControlModuleHandlers { val apps = prefs.getApps(filterType) return mediaControllers?.find { when (filterType) { - SharedPreferencesUtil.AppFilterType.DISABLED -> true - SharedPreferencesUtil.AppFilterType.WHITE_LIST -> it.packageName in apps - SharedPreferencesUtil.AppFilterType.BLACK_LIST -> it.packageName !in apps + AppFilterType.DISABLED -> true + AppFilterType.WHITE_LIST -> it.packageName in apps + AppFilterType.BLACK_LIST -> it.packageName !in apps } }?.also { log("Chosen media controller: ${it.packageName}") } } @@ -295,7 +287,12 @@ object VolumeKeyControlModuleHandlers { log("Sending ${this::class.simpleName}") isLongPress = true sendMediaButtonEventAndTriggerVibration(this) - HookNotifier.incrementLaunchCount(context) + RemotePrefsHelper.withRemotePrefs(context) { + val count = getLaunchedCount() + edit { + putInt(LAUNCHED_COUNT, count + 1) + } + } } object PlayPause : MediaEvent("mVolumeBothLongPress") { diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/HookNotifier.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/HookNotifier.kt deleted file mode 100644 index aab1288..0000000 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/HookNotifier.kt +++ /dev/null @@ -1,24 +0,0 @@ -package ru.hepolise.volumekeytrackcontrol.module.util - -import android.content.Context -import androidx.core.net.toUri -import ru.hepolise.volumekeytrackcontrol.util.Constants -import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig - -object HookNotifier { - - private val authorityUri = "content://${BuildConfig.APPLICATION_ID}.hookstatusprovider".toUri() - - fun notifyHooked(context: Context) { - runCatching { - context.contentResolver.call(authorityUri, Constants.SET_HOOKED, null, null) - } - } - - fun incrementLaunchCount(context: Context) { - runCatching { - context.contentResolver.call(authorityUri, Constants.INCREMENT_LAUNCH_COUNT, null, null) - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/RemotePrefsHelper.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/RemotePrefsHelper.kt new file mode 100644 index 0000000..1d38cd2 --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/RemotePrefsHelper.kt @@ -0,0 +1,26 @@ +package ru.hepolise.volumekeytrackcontrol.module.util + +import android.content.Context +import android.content.SharedPreferences +import com.crossbowffs.remotepreferences.RemotePreferences +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil +import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig + +object RemotePrefsHelper { + private fun log(text: String) = LogHelper.log(RemotePrefsHelper::class.java.simpleName, text) + + fun withRemotePrefs(context: Context, block: SharedPreferences.() -> Unit) { + try { + val prefs = RemotePreferences( + context, + BuildConfig.APPLICATION_ID, + SharedPreferencesUtil.STATUS_PREFS, + true + ) + block.invoke(prefs) + } catch (e: Exception) { + log("remote preferences failed") + log(e.message ?: "unknown") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/StatusHelper.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/StatusHelper.kt new file mode 100644 index 0000000..a1ff6ec --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/StatusHelper.kt @@ -0,0 +1,68 @@ +package ru.hepolise.volumekeytrackcontrol.module.util + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.core.content.edit +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LAST_INIT_HOOK_TIME +import ru.hepolise.volumekeytrackcontrol.util.StatusSysPropsHelper.dynamicKey +import ru.hepolise.volumekeytrackcontrol.util.SystemProps + +object StatusHelper { + private fun log(text: String) = LogHelper.log(StatusHelper::class.java.simpleName, text) + + fun handleSuccessHook(context: Context) { + val eventsLock = Any() + var bootReceived = false + var unlockReceived = false + var anySuccess = false + var remotePrefsSuccess = false + + fun handleEvent(ctx: Context, action: String) { + synchronized(eventsLock) { + if (!remotePrefsSuccess) { + try { + RemotePrefsHelper.withRemotePrefs(ctx) { + edit { + putLong(LAST_INIT_HOOK_TIME, System.currentTimeMillis()) + } + } + remotePrefsSuccess = true + log("Remote prefs updated successfully for $action") + } catch (t: Throwable) { + log("Remote preferences failed for $action (${t.message})") + } + } + + when (action) { + Intent.ACTION_BOOT_COMPLETED -> bootReceived = true + Intent.ACTION_USER_UNLOCKED -> unlockReceived = true + } + anySuccess = anySuccess || remotePrefsSuccess + + if (bootReceived && unlockReceived) { + if (!anySuccess) { + log("Neither event could connect to content-provider, writing to sysprops") + try { + SystemProps.set(dynamicKey(), "1") + } catch (t: Throwable) { + log("Failed to write to sysprops (${t.message})") + } + } + } + } + } + + listOf(Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_USER_UNLOCKED).forEach { intentAction -> + val receiver = object : BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + val action = intent.action.also { log("onReceive: $it") } ?: return + handleEvent(ctx, action) + ctx.unregisterReceiver(this) + } + } + context.registerReceiver(receiver, IntentFilter(intentAction)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/provider/HookStatusProvider.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/provider/HookStatusProvider.kt deleted file mode 100644 index aba75ff..0000000 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/provider/HookStatusProvider.kt +++ /dev/null @@ -1,56 +0,0 @@ -package ru.hepolise.volumekeytrackcontrol.provider - -import android.content.ContentProvider -import android.content.ContentValues -import android.content.Context -import android.database.Cursor -import android.net.Uri -import android.os.Bundle -import androidx.core.content.edit -import ru.hepolise.volumekeytrackcontrol.util.Constants -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.HOOK_PREFS_NAME -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LAST_INIT_HOOK_TIME -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LAUNCHED_COUNT -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLaunchedCount - -class HookStatusProvider : ContentProvider() { - - override fun onCreate(): Boolean = true - - override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { - val context = context ?: return null - val prefs = context.getSharedPreferences(HOOK_PREFS_NAME, Context.MODE_PRIVATE) - - prefs.edit { - when (method) { - Constants.SET_HOOKED -> { - putLong(LAST_INIT_HOOK_TIME, System.currentTimeMillis()) - } - - Constants.INCREMENT_LAUNCH_COUNT -> { - val current = prefs.getLaunchedCount() - putInt(LAUNCHED_COUNT, current + 1) - } - } - } - return null - } - - override fun query( - uri: Uri, - projection: Array?, - selection: String?, - selectionArgs: Array?, - sortOrder: String? - ): Cursor? = null - - override fun getType(uri: Uri): String? = null - override fun insert(uri: Uri, values: ContentValues?): Uri? = null - override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 - override fun update( - uri: Uri, - values: ContentValues?, - selection: String?, - selectionArgs: Array? - ): Int = 0 -} diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/provider/RemotePrefProvider.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/provider/RemotePrefProvider.kt new file mode 100644 index 0000000..842a012 --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/provider/RemotePrefProvider.kt @@ -0,0 +1,16 @@ +package ru.hepolise.volumekeytrackcontrol.provider + +import com.crossbowffs.remotepreferences.RemotePreferenceFile +import com.crossbowffs.remotepreferences.RemotePreferenceProvider +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil +import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig + +class RemotePrefProvider : RemotePreferenceProvider( + BuildConfig.APPLICATION_ID, + arrayOf( + RemotePreferenceFile( + SharedPreferencesUtil.STATUS_PREFS, + true + ) + ) +) \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/BootReceiver.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/BootReceiver.kt new file mode 100644 index 0000000..3e652b3 --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/BootReceiver.kt @@ -0,0 +1,21 @@ +package ru.hepolise.volumekeytrackcontrol.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.core.content.edit +import ru.hepolise.volumekeytrackcontrol.util.LSPosedLogger +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LAST_BOOT_COMPLETED_TIME +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getStatusSharedPreferences + + +class BootReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (Intent.ACTION_BOOT_COMPLETED == intent.action) { + LSPosedLogger.log("Setting last boot completed time from receiver") + context.getStatusSharedPreferences().edit { + putLong(LAST_BOOT_COMPLETED_TIME, System.currentTimeMillis()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/repository/BootRepository.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/repository/BootRepository.kt new file mode 100644 index 0000000..be7a6cb --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/repository/BootRepository.kt @@ -0,0 +1,59 @@ +package ru.hepolise.volumekeytrackcontrol.repository + +import android.content.Context +import android.content.SharedPreferences +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch +import ru.hepolise.volumekeytrackcontrol.util.LSPosedLogger +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LAST_BOOT_COMPLETED_TIME +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getStatusSharedPreferences +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.isBootCompleted +import ru.hepolise.volumekeytrackcontrol.util.StatusSysPropsHelper + +class BootRepository private constructor(private val sharedPreferences: SharedPreferences) { + + companion object { + private var _bootRepository: BootRepository? = null + + fun getBootRepository(context: Context): BootRepository { + return _bootRepository ?: BootRepository( + context.getStatusSharedPreferences() + ).also { _bootRepository = it } + } + } + + + fun isBootCompleted(): Boolean { + return sharedPreferences.isBootCompleted() + } + + fun observeBootCompleted(): Flow = callbackFlow { + try { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + LSPosedLogger.log("Observer: pref changed: $key") + if (key == LAST_BOOT_COMPLETED_TIME) { + LSPosedLogger.log("Boot completed is changed") + launch { + delay(5_000) + }.invokeOnCompletion { + StatusSysPropsHelper.refreshIsHooked() + trySend(isBootCompleted()) + } + } + } + + sharedPreferences.registerOnSharedPreferenceChangeListener(listener) + + if (isBootCompleted()) trySend(true) + + awaitClose { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) + } + } catch (e: Exception) { + close(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/SettingsActivity.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/SettingsActivity.kt index 8d38fcd..de41068 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/SettingsActivity.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/SettingsActivity.kt @@ -2,9 +2,7 @@ package ru.hepolise.volumekeytrackcontrol.ui import android.animation.AnimatorSet import android.animation.ObjectAnimator -import android.annotation.SuppressLint import android.content.Context -import android.content.SharedPreferences import android.os.Build import android.os.Bundle import android.view.View @@ -13,6 +11,7 @@ import android.view.animation.AnticipateInterpolator import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme @@ -21,18 +20,28 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.core.animation.doOnEnd import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import ru.hepolise.volumekeytrackcontrol.ui.navigation.AppNavigation -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.HOOK_PREFS_NAME -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SETTINGS_PREFS_NAME +import ru.hepolise.volumekeytrackcontrol.util.LSPosedLogger +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getSettingsSharedPreferences +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getStatusSharedPreferences import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.isHooked import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.getVibrator +import ru.hepolise.volumekeytrackcontrol.viewmodel.BootViewModel +import ru.hepolise.volumekeytrackcontrol.viewmodel.BootViewModelFactory import kotlin.system.exitProcess class SettingsActivity : ComponentActivity() { + private val bootViewModel: BootViewModel by viewModels { + BootViewModelFactory(applicationContext) + } + @Volatile private var shouldRemoveFromRecents = false @@ -45,7 +54,16 @@ class SettingsActivity : ComponentActivity() { setUpSplashScreenAnimation() enableEdgeToEdge() setContent { - val prefs = tryLoadPrefs() + val hookPrefs = getStatusSharedPreferences() + val prefs = getSettingsSharedPreferences() + + val isLoading by bootViewModel.isLoading.collectAsState() + + LaunchedEffect(hookPrefs, prefs, isLoading) { + LSPosedLogger.log("Updating shouldRemoveFromRecents") + shouldRemoveFromRecents = !hookPrefs.isHooked() || prefs == null + } + MaterialTheme(colorScheme = dynamicColorScheme(context = this)) { AppNavigation(settingsPrefs = prefs, vibrator = getVibrator()) } @@ -59,17 +77,6 @@ class SettingsActivity : ComponentActivity() { } } - private fun Context.tryLoadPrefs(): SharedPreferences? = try { - isHooked = getSharedPreferences(HOOK_PREFS_NAME, MODE_PRIVATE).isHooked() - shouldRemoveFromRecents = !isHooked - @SuppressLint("WorldReadableFiles") - @Suppress("DEPRECATION") - getSharedPreferences(SETTINGS_PREFS_NAME, MODE_WORLD_READABLE) - } catch (_: SecurityException) { - shouldRemoveFromRecents = true - null - } - private fun setUpSplashScreenAnimation() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { return diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/Util.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/Util.kt index 58f44dd..e6c3221 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/Util.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/Util.kt @@ -20,6 +20,4 @@ fun State.debounce( .collect { debouncedState.value = it } } return debouncedState -} - -var isHooked = false \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/AppFilterSetting.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/AppFilterSetting.kt index 072e880..e9f6353 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/AppFilterSetting.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/AppFilterSetting.kt @@ -11,8 +11,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.core.content.edit +import ru.hepolise.volumekeytrackcontrol.util.AppFilterType import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.APP_FILTER_TYPE -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.AppFilterType @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/navigation/AppNavigation.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/navigation/AppNavigation.kt index 335a86c..c4fe7b7 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/navigation/AppNavigation.kt @@ -14,7 +14,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import ru.hepolise.volumekeytrackcontrol.ui.screen.AppFilterScreen import ru.hepolise.volumekeytrackcontrol.ui.screen.SettingsScreen -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil +import ru.hepolise.volumekeytrackcontrol.util.AppFilterType @Composable fun AppNavigation(settingsPrefs: SharedPreferences?, vibrator: Vibrator) { @@ -54,7 +54,7 @@ fun AppNavigation(settingsPrefs: SharedPreferences?, vibrator: Vibrator) { ) } ) { backStackEntry -> - val filterType = SharedPreferencesUtil.AppFilterType.fromKey( + val filterType = AppFilterType.fromKey( backStackEntry.arguments?.getString("filterType") ) AppFilterScreen( diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/AppFilterScreen.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/AppFilterScreen.kt index 8e20251..d07c99d 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/AppFilterScreen.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/AppFilterScreen.kt @@ -102,13 +102,13 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import ru.hepolise.volumekeytrackcontrol.ui.debounce import ru.hepolise.volumekeytrackcontrol.ui.model.AppInfo -import ru.hepolise.volumekeytrackcontrol.ui.viewmodel.AppFilterViewModel -import ru.hepolise.volumekeytrackcontrol.ui.viewmodel.AppIconViewModel -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil +import ru.hepolise.volumekeytrackcontrol.util.AppFilterType import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.BLACK_LIST_APPS -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SETTINGS_PREFS_NAME +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SETTINGS_PREFS import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.WHITE_LIST_APPS import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getApps +import ru.hepolise.volumekeytrackcontrol.viewmodel.AppFilterViewModel +import ru.hepolise.volumekeytrackcontrol.viewmodel.AppIconViewModel import ru.hepolise.volumekeytrackcontrolmodule.R private const val MAX_APPS = 100 @@ -118,7 +118,7 @@ private val LETTERS = ('A'..'Z').toList() @OptIn(ExperimentalMaterial3Api::class) @Composable fun AppFilterScreen( - filterType: SharedPreferencesUtil.AppFilterType, + filterType: AppFilterType, sharedPreferences: SharedPreferences, navController: NavController? = null, viewModel: AppFilterViewModel = viewModel(), @@ -193,8 +193,8 @@ fun AppFilterScreen( sharedPreferences.edit { putStringSet( when (filterType) { - SharedPreferencesUtil.AppFilterType.BLACK_LIST -> BLACK_LIST_APPS - SharedPreferencesUtil.AppFilterType.WHITE_LIST -> WHITE_LIST_APPS + AppFilterType.BLACK_LIST -> BLACK_LIST_APPS + AppFilterType.WHITE_LIST -> WHITE_LIST_APPS else -> throw IllegalStateException("Invalid filter type: $filterType") }, selectedApps.toSet() @@ -686,9 +686,9 @@ private class AppListComparator( @Composable fun PreviewAppFilterScreen() { AppFilterScreen( - filterType = SharedPreferencesUtil.AppFilterType.WHITE_LIST, + filterType = AppFilterType.WHITE_LIST, sharedPreferences = LocalContext.current.getSharedPreferences( - SETTINGS_PREFS_NAME, + SETTINGS_PREFS, Context.MODE_PRIVATE, ), ) diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt index 04f4f6e..4d788c1 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt @@ -44,6 +44,7 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -55,11 +56,15 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -84,31 +89,35 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.content.edit import androidx.core.net.toUri +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import ru.hepolise.volumekeytrackcontrol.ui.component.AppFilterSetting import ru.hepolise.volumekeytrackcontrol.ui.component.LongPressSetting import ru.hepolise.volumekeytrackcontrol.ui.component.SwapButtonsSetting import ru.hepolise.volumekeytrackcontrol.ui.component.VibrationEffectSetting import ru.hepolise.volumekeytrackcontrol.ui.component.VibrationSettingData -import ru.hepolise.volumekeytrackcontrol.ui.isHooked +import ru.hepolise.volumekeytrackcontrol.util.AppFilterType import ru.hepolise.volumekeytrackcontrol.util.Constants -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.APP_FILTER_TYPE_DEFAULT_VALUE import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.EFFECT_DEFAULT_VALUE -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.HOOK_PREFS_NAME import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.IS_SWAP_BUTTONS_DEFAULT_VALUE import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LONG_PRESS_DURATION_DEFAULT_VALUE -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SETTINGS_PREFS_NAME +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SETTINGS_PREFS import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_AMPLITUDE_DEFAULT_VALUE import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_LENGTH_DEFAULT_VALUE import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getAppFilterType import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLaunchedCount import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLongPressDuration +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getStatusSharedPreferences import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getVibrationAmplitude import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getVibrationLength import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getVibrationType +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.isHooked import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.isSwapButtons +import ru.hepolise.volumekeytrackcontrol.util.StatusSysPropsHelper import ru.hepolise.volumekeytrackcontrol.util.VibrationType +import ru.hepolise.volumekeytrackcontrol.viewmodel.BootViewModel +import ru.hepolise.volumekeytrackcontrol.viewmodel.BootViewModelFactory import ru.hepolise.volumekeytrackcontrolmodule.R @OptIn(ExperimentalMaterial3Api::class) @@ -120,7 +129,14 @@ fun SettingsScreen( ) { val context = LocalContext.current - val hookPrefs = context.getSharedPreferences(HOOK_PREFS_NAME, Context.MODE_PRIVATE) + val statusPrefs = context.getStatusSharedPreferences() + + val bootViewModel: BootViewModel = viewModel( + factory = BootViewModelFactory(context.applicationContext) + ) + + val isBootCompleted by bootViewModel.isBootCompleted.collectAsState() + val isLoading by bootViewModel.isLoading.collectAsState() var longPressDuration by remember { mutableIntStateOf(settingsPrefs.getLongPressDuration()) } var vibrationType by remember { mutableStateOf(settingsPrefs.getVibrationType()) } @@ -130,13 +146,24 @@ fun SettingsScreen( var appFilterType by remember { mutableStateOf(settingsPrefs.getAppFilterType()) } var showResetSettingsDialog by remember { mutableStateOf(false) } - val isHooked = isHooked.takeIf { settingsPrefs != null } ?: false - var launchedCount by remember { mutableIntStateOf(hookPrefs.getLaunchedCount()) } + val isHooked by produceState(initialValue = false) { + value = statusPrefs.isHooked().takeIf { settingsPrefs != null } ?: false + + snapshotFlow { isLoading }.collect { + value = statusPrefs.isHooked().takeIf { settingsPrefs != null } ?: false + } + } + var launchedCount by remember { mutableIntStateOf(statusPrefs.getLaunchedCount()) } - val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> - launchedCount = hookPrefs.getLaunchedCount() + DisposableEffect(Unit) { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> + launchedCount = statusPrefs.getLaunchedCount() + } + statusPrefs.registerOnSharedPreferenceChangeListener(listener) + onDispose { + statusPrefs.unregisterOnSharedPreferenceChangeListener(listener) + } } - hookPrefs.registerOnSharedPreferenceChangeListener(listener) val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() @@ -181,28 +208,15 @@ fun SettingsScreen( icon = if (isHooked) Icons.Default.Done else Icons.Default.Warning, title = stringResource(R.string.module_info), ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - stringResource(R.string.module_status), - style = MaterialTheme.typography.bodyMedium - ) - Text( - text = stringResource( - if (isHooked) R.string.module_status_active - else R.string.module_status_inactive - ), - color = if (isHooked) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.error, - fontWeight = FontWeight.Bold - ) + if (isLoading && !isHooked) { + LoadingAnimation() + } else { + ModuleStatus(isHooked) } when { - isHooked -> ModuleIsEnabled(launchedCount) + isHooked -> if (!StatusSysPropsHelper.isHooked) LaunchCounter(launchedCount) settingsPrefs == null -> ModuleIsNotEnabled() - else -> ModuleInitError() + isBootCompleted -> ModuleInitError() } } @@ -244,7 +258,7 @@ fun SettingsScreen( SettingsCard( icon = Icons.Default.Star, title = stringResource(R.string.app_filter), - showAction = appFilterType != SharedPreferencesUtil.AppFilterType.DISABLED, + showAction = appFilterType != AppFilterType.DISABLED, onActionClick = { navController?.navigate("appFilter/${appFilterType.key}") } ) { AppFilterSetting( @@ -267,7 +281,7 @@ fun SettingsScreen( vibrationAmplitude = VIBRATION_AMPLITUDE_DEFAULT_VALUE longPressDuration = LONG_PRESS_DURATION_DEFAULT_VALUE isSwapButtons = IS_SWAP_BUTTONS_DEFAULT_VALUE - appFilterType = SharedPreferencesUtil.AppFilterType.fromKey( + appFilterType = AppFilterType.fromKey( APP_FILTER_TYPE_DEFAULT_VALUE ) Toast.makeText( @@ -399,7 +413,7 @@ fun ActionIconButton( } @Composable -fun ModuleIsEnabled(launchedCount: Int) { +fun LaunchCounter(launchedCount: Int) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween @@ -415,6 +429,45 @@ fun ModuleIsEnabled(launchedCount: Int) { } } +@Composable +fun LoadingAnimation() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator( + modifier = Modifier.size(48.dp), + color = MaterialTheme.colorScheme.primary, + strokeWidth = 4.dp + ) + } +} + +@Composable +fun ModuleStatus(isHooked: Boolean) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.module_status), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = stringResource( + if (isHooked) R.string.module_status_active + else R.string.module_status_inactive + ), + color = if (isHooked) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.error, + fontWeight = FontWeight.Bold + ) + } +} + @Composable fun ModuleIsNotEnabled() { Text( @@ -479,7 +532,7 @@ fun PreviewSettingsScreen() { SettingsScreen( navController = null, settingsPrefs = LocalContext.current.getSharedPreferences( - SETTINGS_PREFS_NAME, + SETTINGS_PREFS, Context.MODE_PRIVATE ), vibrator = null diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/AppFilterType.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/AppFilterType.kt new file mode 100644 index 0000000..87dbcde --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/AppFilterType.kt @@ -0,0 +1,17 @@ +package ru.hepolise.volumekeytrackcontrol.util + +import ru.hepolise.volumekeytrackcontrolmodule.R + +enum class AppFilterType( + val value: Int, + val key: String, + val resourceId: Int +) { + DISABLED(0, "disabled", R.string.app_filter_disabled), + WHITE_LIST(1, "whitelist", R.string.app_filter_white_list), + BLACK_LIST(2, "blacklist", R.string.app_filter_black_list); + + companion object { + fun fromKey(key: String?) = entries.find { it.key == key } ?: DISABLED + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/LSPosedLogger.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/LSPosedLogger.kt new file mode 100644 index 0000000..ab3e42d --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/LSPosedLogger.kt @@ -0,0 +1,10 @@ +package ru.hepolise.volumekeytrackcontrol.util + +import android.util.Log +import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig + +object LSPosedLogger { + fun log(text: String) { + if (BuildConfig.DEBUG) Log.d("LSPosed-Bridge", text) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SharedPreferencesUtil.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SharedPreferencesUtil.kt index 5596245..5aae451 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SharedPreferencesUtil.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SharedPreferencesUtil.kt @@ -1,16 +1,19 @@ package ru.hepolise.volumekeytrackcontrol.util +import android.annotation.SuppressLint +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.content.Context.MODE_WORLD_READABLE import android.content.SharedPreferences import android.os.Build import android.os.SystemClock import android.view.ViewConfiguration import de.robv.android.xposed.XSharedPreferences import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig -import ru.hepolise.volumekeytrackcontrolmodule.R object SharedPreferencesUtil { - const val SETTINGS_PREFS_NAME = "settings_prefs" - const val HOOK_PREFS_NAME = "hook_prefs" + const val SETTINGS_PREFS = "settings_prefs" + const val STATUS_PREFS = "status_prefs" const val EFFECT = "selectedEffect" const val VIBRATION_LENGTH = "vibrationLength" @@ -23,6 +26,7 @@ object SharedPreferencesUtil { const val LAST_INIT_HOOK_TIME = "lastInitHookTime" const val LAUNCHED_COUNT = "launchedCount" + const val LAST_BOOT_COMPLETED_TIME = "lastBootCompletedTime" val EFFECT_DEFAULT_VALUE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) VibrationType.Click.key else VibrationType.Manual.key @@ -75,9 +79,17 @@ object SharedPreferencesUtil { } } - fun SharedPreferences.isHooked(): Boolean = + fun SharedPreferences.isHooked(): Boolean { + val hookTime by lazy { + getLong(LAST_INIT_HOOK_TIME, 0L) + } + return StatusSysPropsHelper.isHooked + || hookTime >= (System.currentTimeMillis() - SystemClock.elapsedRealtime()) + } + + fun SharedPreferences.isBootCompleted() = getLong( - LAST_INIT_HOOK_TIME, + LAST_BOOT_COMPLETED_TIME, 0L ) >= (System.currentTimeMillis() - SystemClock.elapsedRealtime()) @@ -87,22 +99,17 @@ object SharedPreferencesUtil { private var _prefs: SharedPreferences? = null fun prefs(): SharedPreferences? = - XSharedPreferences(BuildConfig.APPLICATION_ID, SETTINGS_PREFS_NAME) + XSharedPreferences(BuildConfig.APPLICATION_ID, SETTINGS_PREFS) .takeIf { it.file.canRead() } ?.also { _prefs = it } ?: _prefs - enum class AppFilterType( - val value: Int, - val key: String, - val resourceId: Int - ) { - DISABLED(0, "disabled", R.string.app_filter_disabled), - WHITE_LIST(1, "whitelist", R.string.app_filter_white_list), - BLACK_LIST(2, "blacklist", R.string.app_filter_black_list); - - companion object { - fun fromKey(key: String?) = entries.find { it.key == key } ?: DISABLED - } - } + fun Context.getSettingsSharedPreferences(): SharedPreferences? = runCatching { + @SuppressLint("WorldReadableFiles") + @Suppress("DEPRECATION") + return getSharedPreferences(SETTINGS_PREFS, MODE_WORLD_READABLE) + }.getOrNull() + + fun Context.getStatusSharedPreferences(): SharedPreferences = + getSharedPreferences(STATUS_PREFS, MODE_PRIVATE) } \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/StatusSysPropsHelper.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/StatusSysPropsHelper.kt new file mode 100644 index 0000000..52203a8 --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/StatusSysPropsHelper.kt @@ -0,0 +1,44 @@ +package ru.hepolise.volumekeytrackcontrol.util + +import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig +import java.io.File +import java.security.MessageDigest +import java.util.Locale + +object StatusSysPropsHelper { + + private fun readBootIdShort(): String? { + try { + val f = File("/proc/sys/kernel/random/boot_id") + if (f.exists()) { + val id = f.readText().trim() + if (id.isNotEmpty()) { + return sha256(id).substring(0, 8).lowercase(Locale.US) + } + } + } catch (_: Throwable) { /* ignore */ + } + return null + } + + private fun sha256(input: String): String { + val md = MessageDigest.getInstance("SHA-256") + val digest = md.digest("$input:${BuildConfig.APPLICATION_ID}".toByteArray(Charsets.UTF_8)) + return digest.joinToString("") { "%02x".format(it) } + } + + fun dynamicKey(): String { + val suffix = readBootIdShort() ?: "noboot" + return "sys.$suffix" + } + + private var _isHooked: Boolean? = null + val isHooked: Boolean + get() = _isHooked ?: run { + (SystemProps.get(dynamicKey()) == "1").also { _isHooked = it } + } + + fun refreshIsHooked() { + _isHooked = null + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SystemProps.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SystemProps.kt new file mode 100644 index 0000000..73b064c --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SystemProps.kt @@ -0,0 +1,30 @@ +package ru.hepolise.volumekeytrackcontrol.util + +import ru.hepolise.volumekeytrackcontrol.module.util.LogHelper + +@Suppress("PrivateApi") +object SystemProps { + private fun log(text: String) = + LogHelper.log(SystemProps::class.java.simpleName, text) + + private val clazz by lazy { Class.forName("android.os.SystemProperties") } + private val getString by lazy { clazz.getMethod("get", String::class.java, String::class.java) } + private val setString by lazy { clazz.getMethod("set", String::class.java, String::class.java) } + + fun get(key: String, def: String = ""): String { + return try { + getString.invoke(null, key, def) as String + } catch (_: Throwable) { + def + } + } + + fun set(key: String, value: String) { + try { + setString.invoke(null, key, value) + log("set $key to $value") + } catch (t: Throwable) { + log("set $key to $value failed: ${t.message}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/viewmodel/AppFilterViewModel.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/AppFilterViewModel.kt similarity index 96% rename from app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/viewmodel/AppFilterViewModel.kt rename to app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/AppFilterViewModel.kt index 8fce3be..9468fb6 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/viewmodel/AppFilterViewModel.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/AppFilterViewModel.kt @@ -1,4 +1,4 @@ -package ru.hepolise.volumekeytrackcontrol.ui.viewmodel +package ru.hepolise.volumekeytrackcontrol.viewmodel import android.content.Context import android.content.pm.PackageManager diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/viewmodel/AppIconViewModel.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/AppIconViewModel.kt similarity index 95% rename from app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/viewmodel/AppIconViewModel.kt rename to app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/AppIconViewModel.kt index 8ca9a03..c3df388 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/viewmodel/AppIconViewModel.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/AppIconViewModel.kt @@ -1,4 +1,4 @@ -package ru.hepolise.volumekeytrackcontrol.ui.viewmodel +package ru.hepolise.volumekeytrackcontrol.viewmodel import android.app.Application import android.graphics.Bitmap diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/BootViewModel.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/BootViewModel.kt new file mode 100644 index 0000000..9a70747 --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/BootViewModel.kt @@ -0,0 +1,56 @@ +package ru.hepolise.volumekeytrackcontrol.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import ru.hepolise.volumekeytrackcontrol.repository.BootRepository +import ru.hepolise.volumekeytrackcontrol.util.LSPosedLogger +import ru.hepolise.volumekeytrackcontrol.util.StatusSysPropsHelper + +class BootViewModel( + private val bootRepository: BootRepository +) : ViewModel() { + + private val _isBootCompleted = MutableStateFlow(false) + val isBootCompleted = _isBootCompleted.asStateFlow() + + private val _isLoading = MutableStateFlow(true) + val isLoading = _isLoading.asStateFlow() + + private var observationJob: Job? = null + + init { + LSPosedLogger.log("Init view model") + observationJob = viewModelScope.launch { + bootRepository.observeBootCompleted().collect { completed -> + _isBootCompleted.value = completed + _isLoading.value = false + observationJob?.cancel() + } + } + + viewModelScope.launch { + if (!bootRepository.isBootCompleted()) { + checkBootStatus() + } + } + } + + private suspend fun checkBootStatus() { + delay(60_000) + StatusSysPropsHelper.refreshIsHooked() + if (!_isBootCompleted.value) { + LSPosedLogger.log("By timer, boot is not still completed") + _isLoading.value = false + } + } + + override fun onCleared() { + observationJob?.cancel() + super.onCleared() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/BootViewModelFactory.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/BootViewModelFactory.kt new file mode 100644 index 0000000..3502c4b --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/BootViewModelFactory.kt @@ -0,0 +1,17 @@ +package ru.hepolise.volumekeytrackcontrol.viewmodel + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import ru.hepolise.volumekeytrackcontrol.repository.BootRepository + +class BootViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(BootViewModel::class.java)) { + val repository = BootRepository.getBootRepository(context) + return BootViewModel(repository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file From 020de776ed12a2d62364db95b36f6e8befeb466a Mon Sep 17 00:00:00 2001 From: Hepolise Date: Sun, 19 Oct 2025 16:04:01 +0300 Subject: [PATCH 05/10] fix: module status detection - Add utility to check if app was installed after system reboot - Update SettingsScreen to show proper module status for fresh installations - Only show loading animation and error states when settings are initialized --- .../hepolise/volumekeytrackcontrol/ui/Util.kt | 18 ++++++++++++++++ .../ui/screen/SettingsScreen.kt | 21 ++++++++++++------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/Util.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/Util.kt index e6c3221..3dc365b 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/Util.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/Util.kt @@ -1,5 +1,8 @@ package ru.hepolise.volumekeytrackcontrol.ui +import android.content.Context +import android.content.pm.PackageManager +import android.os.SystemClock import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshotFlow @@ -20,4 +23,19 @@ fun State.debounce( .collect { debouncedState.value = it } } return debouncedState +} + +private fun Context.getAppUpdateTime(): Long { + return try { + val packageInfo = packageManager.getPackageInfo(packageName, 0) + packageInfo.lastUpdateTime + } catch (e: PackageManager.NameNotFoundException) { + -1 + } +} + +fun Context.isInstalledAfterReboot(): Boolean { + val installTime = getAppUpdateTime() + val bootTime = System.currentTimeMillis() - SystemClock.elapsedRealtime() + return installTime > bootTime } \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt index 4d788c1..22aa2df 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt @@ -96,6 +96,7 @@ import ru.hepolise.volumekeytrackcontrol.ui.component.LongPressSetting import ru.hepolise.volumekeytrackcontrol.ui.component.SwapButtonsSetting import ru.hepolise.volumekeytrackcontrol.ui.component.VibrationEffectSetting import ru.hepolise.volumekeytrackcontrol.ui.component.VibrationSettingData +import ru.hepolise.volumekeytrackcontrol.ui.isInstalledAfterReboot import ru.hepolise.volumekeytrackcontrol.util.AppFilterType import ru.hepolise.volumekeytrackcontrol.util.Constants import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.APP_FILTER_TYPE_DEFAULT_VALUE @@ -208,15 +209,19 @@ fun SettingsScreen( icon = if (isHooked) Icons.Default.Done else Icons.Default.Warning, title = stringResource(R.string.module_info), ) { - if (isLoading && !isHooked) { - LoadingAnimation() + if (context.isInstalledAfterReboot()) { + ModuleStatus(false) } else { - ModuleStatus(isHooked) - } - when { - isHooked -> if (!StatusSysPropsHelper.isHooked) LaunchCounter(launchedCount) - settingsPrefs == null -> ModuleIsNotEnabled() - isBootCompleted -> ModuleInitError() + if (isLoading && !isHooked && settingsPrefs != null) { + LoadingAnimation() + } else { + ModuleStatus(isHooked) + } + when { + isHooked -> if (!StatusSysPropsHelper.isHooked) LaunchCounter(launchedCount) + settingsPrefs == null -> ModuleIsNotEnabled() + isBootCompleted -> ModuleInitError() + } } } From 16ce43c11c818efdd5e2f3f0d544e7fc604dc267 Mon Sep 17 00:00:00 2001 From: Hepolise Date: Sun, 19 Oct 2025 16:08:03 +0300 Subject: [PATCH 06/10] fix: module status display logic in settings screen - Restructure module status display to handle post-reboot installation case - Show module init error when app is installed after reboot - Improve loading state handling during module initialization --- .../ui/screen/SettingsScreen.kt | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt index 22aa2df..051e3a4 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt @@ -209,18 +209,27 @@ fun SettingsScreen( icon = if (isHooked) Icons.Default.Done else Icons.Default.Warning, title = stringResource(R.string.module_info), ) { - if (context.isInstalledAfterReboot()) { - ModuleStatus(false) - } else { - if (isLoading && !isHooked && settingsPrefs != null) { + when { + context.isInstalledAfterReboot() -> { + ModuleStatus(false) + ModuleInitError() + } + + isLoading && !isHooked && settingsPrefs != null -> { LoadingAnimation() - } else { - ModuleStatus(isHooked) } - when { - isHooked -> if (!StatusSysPropsHelper.isHooked) LaunchCounter(launchedCount) - settingsPrefs == null -> ModuleIsNotEnabled() - isBootCompleted -> ModuleInitError() + + else -> { + ModuleStatus(isHooked) + + when { + isHooked && !StatusSysPropsHelper.isHooked -> LaunchCounter( + launchedCount + ) + + settingsPrefs == null -> ModuleIsNotEnabled() + isBootCompleted -> ModuleInitError() + } } } } From 840c0c15a0f1c5b080e4787de92c2b30d985bb99 Mon Sep 17 00:00:00 2001 From: Hepolise Date: Sun, 19 Oct 2025 16:54:19 +0300 Subject: [PATCH 07/10] fix: remote preferences initialization by removing unnecessary error suppression --- .../module/util/RemotePrefsHelper.kt | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/RemotePrefsHelper.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/RemotePrefsHelper.kt index 1d38cd2..c40f2d2 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/RemotePrefsHelper.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/RemotePrefsHelper.kt @@ -10,17 +10,12 @@ object RemotePrefsHelper { private fun log(text: String) = LogHelper.log(RemotePrefsHelper::class.java.simpleName, text) fun withRemotePrefs(context: Context, block: SharedPreferences.() -> Unit) { - try { - val prefs = RemotePreferences( - context, - BuildConfig.APPLICATION_ID, - SharedPreferencesUtil.STATUS_PREFS, - true - ) - block.invoke(prefs) - } catch (e: Exception) { - log("remote preferences failed") - log(e.message ?: "unknown") - } + val prefs = RemotePreferences( + context, + BuildConfig.APPLICATION_ID, + SharedPreferencesUtil.STATUS_PREFS, + true + ) + block.invoke(prefs) } } \ No newline at end of file From e3b7c60826beeeb81a019b91dd3ce1268df19614 Mon Sep 17 00:00:00 2001 From: Hepolise Date: Sun, 19 Oct 2025 17:11:31 +0300 Subject: [PATCH 08/10] # Fix boot detection and module init check **Changes:** - Added `setBootCompleted()` to persist boot time - Changed boot check to poll for hook detection (1s intervals, 60 attempts) - Only show module error when boot completed AND hook not detected - Improved boot detection logging --- .../receiver/BootReceiver.kt | 8 ++----- .../repository/BootRepository.kt | 7 ++++++ .../ui/screen/SettingsScreen.kt | 2 +- .../viewmodel/BootViewModel.kt | 24 +++++++++++++++---- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/BootReceiver.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/BootReceiver.kt index 3e652b3..b84b6d3 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/BootReceiver.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/BootReceiver.kt @@ -3,19 +3,15 @@ package ru.hepolise.volumekeytrackcontrol.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import androidx.core.content.edit +import ru.hepolise.volumekeytrackcontrol.repository.BootRepository import ru.hepolise.volumekeytrackcontrol.util.LSPosedLogger -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LAST_BOOT_COMPLETED_TIME -import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getStatusSharedPreferences class BootReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (Intent.ACTION_BOOT_COMPLETED == intent.action) { LSPosedLogger.log("Setting last boot completed time from receiver") - context.getStatusSharedPreferences().edit { - putLong(LAST_BOOT_COMPLETED_TIME, System.currentTimeMillis()) - } + BootRepository.getBootRepository(context).setBootCompleted() } } } \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/repository/BootRepository.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/repository/BootRepository.kt index be7a6cb..844cdf7 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/repository/BootRepository.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/repository/BootRepository.kt @@ -2,6 +2,7 @@ package ru.hepolise.volumekeytrackcontrol.repository import android.content.Context import android.content.SharedPreferences +import androidx.core.content.edit import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -30,6 +31,12 @@ class BootRepository private constructor(private val sharedPreferences: SharedPr return sharedPreferences.isBootCompleted() } + fun setBootCompleted() { + sharedPreferences.edit { + putLong(LAST_BOOT_COMPLETED_TIME, System.currentTimeMillis()) + } + } + fun observeBootCompleted(): Flow = callbackFlow { try { val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt index 051e3a4..cc8b23b 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt @@ -228,7 +228,7 @@ fun SettingsScreen( ) settingsPrefs == null -> ModuleIsNotEnabled() - isBootCompleted -> ModuleInitError() + isBootCompleted && !isHooked -> ModuleInitError() } } } diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/BootViewModel.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/BootViewModel.kt index 9a70747..2fa7bf6 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/BootViewModel.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/BootViewModel.kt @@ -40,13 +40,27 @@ class BootViewModel( } } + private suspend fun checkBootStatus() { - delay(60_000) - StatusSysPropsHelper.refreshIsHooked() - if (!_isBootCompleted.value) { - LSPosedLogger.log("By timer, boot is not still completed") - _isLoading.value = false + val maxAttempts = 60 + var attempts = 0 + + while (attempts < maxAttempts) { + delay(1_000) + StatusSysPropsHelper.refreshIsHooked() + + if (StatusSysPropsHelper.isHooked) { + LSPosedLogger.log("Boot completed - hook detected") + bootRepository.setBootCompleted() + return + } + + attempts++ + LSPosedLogger.log("Attempt $attempts: hook not detected yet") } + + LSPosedLogger.log("By timer, boot is not still completed after $maxAttempts attempts") + _isLoading.value = false } override fun onCleared() { From 52b25430eaf3e0107b9bf4623ff53bfbd9360d50 Mon Sep 17 00:00:00 2001 From: Hepolise Date: Sun, 19 Oct 2025 17:14:22 +0300 Subject: [PATCH 09/10] fix: handle exceptions when tracking long-press launch count - Wrap RemotePrefsHelper usage in runCatching block - Prevents crashes during long-press volume key actions --- .../module/VolumeKeyControlModuleHandlers.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt index d066a3b..7a37922 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt @@ -287,10 +287,12 @@ object VolumeKeyControlModuleHandlers { log("Sending ${this::class.simpleName}") isLongPress = true sendMediaButtonEventAndTriggerVibration(this) - RemotePrefsHelper.withRemotePrefs(context) { - val count = getLaunchedCount() - edit { - putInt(LAUNCHED_COUNT, count + 1) + runCatching { + RemotePrefsHelper.withRemotePrefs(context) { + val count = getLaunchedCount() + edit { + putInt(LAUNCHED_COUNT, count + 1) + } } } } From 70337b529f759e12a23e4137440440a2318dde4c Mon Sep 17 00:00:00 2001 From: Hepolise Date: Sun, 19 Oct 2025 17:26:07 +0300 Subject: [PATCH 10/10] fix: Hide long press settings when app is installed after reboot --- .../hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt index cc8b23b..a35de31 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt @@ -234,7 +234,7 @@ fun SettingsScreen( } } - if (settingsPrefs != null && isHooked) { + if (settingsPrefs != null && isHooked && !context.isInstalledAfterReboot()) { SettingsCard( icon = Icons.Default.Settings, title = stringResource(R.string.long_press_settings)