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 9f63baa..dc8fccb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
+
+
+ android:name="ru.hepolise.volumekeytrackcontrol.receiver.BootReceiver"
+ android:enabled="true"
+ android:exported="true">
-
+
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeControlModule.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeControlModule.kt
index 7a9e3c3..a8ad82e 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeControlModule.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeControlModule.kt
@@ -54,15 +54,16 @@ class VolumeControlModule : IXposedHookLoadPackage {
@Throws(Throwable::class)
override fun handleLoadPackage(lpparam: LoadPackageParam) {
log("handleLoadPackage: ${lpparam.packageName}")
- if (lpparam.packageName != "android") {
- return
+ with(lpparam) {
+ when (packageName) {
+ "android" -> 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 f34eb0b..7a37922 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt
@@ -1,10 +1,6 @@
package ru.hepolise.volumekeytrackcontrol.module
-import android.content.BroadcastReceiver
-import android.content.ComponentName
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
@@ -15,20 +11,23 @@ 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.LogHelper
-import ru.hepolise.volumekeytrackcontrol.receiver.HookBroadcastReceiver
-import ru.hepolise.volumekeytrackcontrol.util.Constants
+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
import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.triggerVibration
-import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig
object VolumeKeyControlModuleHandlers {
@@ -82,15 +81,7 @@ object VolumeKeyControlModuleHandlers {
XposedHelpers.setAdditionalInstanceField(param.thisObject, event.field, runnable)
}
- val filter = IntentFilter(Intent.ACTION_USER_UNLOCKED)
- context.registerReceiver(object : BroadcastReceiver() {
- override fun onReceive(ctx: Context, intent: Intent) {
- context.sendBroadcast {
- putExtra(Constants.HOOKED, true)
- }
- ctx.unregisterReceiver(this)
- }
- }, filter)
+ StatusHelper.handleSuccessHook(context)
}
}
@@ -224,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}") }
}
@@ -255,21 +246,6 @@ object VolumeKeyControlModuleHandlers {
}
}
- private fun Context.sendBroadcast(block: Intent.() -> 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,8 +287,13 @@ object VolumeKeyControlModuleHandlers {
log("Sending ${this::class.simpleName}")
isLongPress = true
sendMediaButtonEventAndTriggerVibration(this)
- context.sendBroadcast {
- putExtra(Constants.INCREMENT_LAUNCH_COUNT, true)
+ runCatching {
+ RemotePrefsHelper.withRemotePrefs(context) {
+ val count = getLaunchedCount()
+ edit {
+ putInt(LAUNCHED_COUNT, count + 1)
+ }
+ }
}
}
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..c40f2d2
--- /dev/null
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/RemotePrefsHelper.kt
@@ -0,0 +1,21 @@
+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) {
+ val prefs = RemotePreferences(
+ context,
+ BuildConfig.APPLICATION_ID,
+ SharedPreferencesUtil.STATUS_PREFS,
+ true
+ )
+ block.invoke(prefs)
+ }
+}
\ 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/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..b84b6d3
--- /dev/null
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/BootReceiver.kt
@@ -0,0 +1,17 @@
+package ru.hepolise.volumekeytrackcontrol.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import ru.hepolise.volumekeytrackcontrol.repository.BootRepository
+import ru.hepolise.volumekeytrackcontrol.util.LSPosedLogger
+
+
+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")
+ BootRepository.getBootRepository(context).setBootCompleted()
+ }
+ }
+}
\ No newline at end of file
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/repository/BootRepository.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/repository/BootRepository.kt
new file mode 100644
index 0000000..844cdf7
--- /dev/null
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/repository/BootRepository.kt
@@ -0,0 +1,66 @@
+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
+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 setBootCompleted() {
+ sharedPreferences.edit {
+ putLong(LAST_BOOT_COMPLETED_TIME, System.currentTimeMillis())
+ }
+ }
+
+ 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..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
@@ -22,4 +25,17 @@ fun State.debounce(
return debouncedState
}
-var isHooked = false
\ No newline at end of file
+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/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..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
@@ -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,36 @@ 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.ui.isInstalledAfterReboot
+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 +130,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 +147,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
- val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
- launchedCount = hookPrefs.getLaunchedCount()
+ snapshotFlow { isLoading }.collect {
+ value = statusPrefs.isHooked().takeIf { settingsPrefs != null } ?: false
+ }
+ }
+ var launchedCount by remember { mutableIntStateOf(statusPrefs.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,32 +209,32 @@ 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
- )
- }
when {
- isHooked -> ModuleIsEnabled(launchedCount)
- settingsPrefs == null -> ModuleIsNotEnabled()
- else -> ModuleInitError()
+ context.isInstalledAfterReboot() -> {
+ ModuleStatus(false)
+ ModuleInitError()
+ }
+
+ isLoading && !isHooked && settingsPrefs != null -> {
+ LoadingAnimation()
+ }
+
+ else -> {
+ ModuleStatus(isHooked)
+
+ when {
+ isHooked && !StatusSysPropsHelper.isHooked -> LaunchCounter(
+ launchedCount
+ )
+
+ settingsPrefs == null -> ModuleIsNotEnabled()
+ isBootCompleted && !isHooked -> ModuleInitError()
+ }
+ }
}
}
- if (settingsPrefs != null && isHooked) {
+ if (settingsPrefs != null && isHooked && !context.isInstalledAfterReboot()) {
SettingsCard(
icon = Icons.Default.Settings,
title = stringResource(R.string.long_press_settings)
@@ -244,7 +272,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 +295,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 +427,7 @@ fun ActionIconButton(
}
@Composable
-fun ModuleIsEnabled(launchedCount: Int) {
+fun LaunchCounter(launchedCount: Int) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
@@ -415,6 +443,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 +546,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/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
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..2fa7bf6
--- /dev/null
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/viewmodel/BootViewModel.kt
@@ -0,0 +1,70 @@
+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() {
+ 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() {
+ 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