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