diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c0dfd5e..5d52335 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,7 +10,7 @@ jobs:
contents: read
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 489edef..181296d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -23,10 +23,16 @@
+
+
+
@@ -66,11 +72,14 @@
+
+
+
{
+ Log.i(TAG, "Boot completed, starting broadcast receiver service...")
+ val serviceIntent = Intent(context, BroadcastReceiverService::class.java)
+ ContextCompat.startForegroundService(context, serviceIntent)
+ }
+
+ Intent.ACTION_MY_PACKAGE_REPLACED -> {
+ Log.i(TAG, "App package replaced, starting broadcast receiver service if needed...")
+ val serviceIntent = Intent(context, BroadcastReceiverService::class.java)
+ ContextCompat.startForegroundService(context, serviceIntent)
+ }
+
+ else -> {
+ Log.d(TAG, "Received unrelated intent action: ${intent.action}")
+ }
}
}
diff --git a/app/src/main/java/dev/lexip/hecate/broadcasts/ScreenOnReceiver.kt b/app/src/main/java/dev/lexip/hecate/broadcasts/ScreenOnReceiver.kt
index 47583ff..c2f575d 100644
--- a/app/src/main/java/dev/lexip/hecate/broadcasts/ScreenOnReceiver.kt
+++ b/app/src/main/java/dev/lexip/hecate/broadcasts/ScreenOnReceiver.kt
@@ -38,18 +38,17 @@ class ScreenOnReceiver(
Log.d(TAG, "Screen turned on, checking adaptive theme conditions...")
// Check if the device is covered using the proximity sensor
- proximitySensorManager.startListening { distance ->
+ proximitySensorManager.startListening({ distance: Float ->
proximitySensorManager.stopListening()
// If the device is not covered, change the device theme based on the environment
if (distance >= 5f) {
- lightSensorManager.startListening { lightValue ->
+ lightSensorManager.startListening({ lightValue: Float ->
lightSensorManager.stopListening()
darkThemeHandler.setDarkTheme(lightValue < adaptiveThemeThresholdLux)
- }
+ })
} else Log.d(TAG, "Device is covered, skipping adaptive theme checks.")
-
- }
+ })
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/lexip/hecate/services/BroadcastReceiverService.kt b/app/src/main/java/dev/lexip/hecate/services/BroadcastReceiverService.kt
index 5438d9f..1007a78 100644
--- a/app/src/main/java/dev/lexip/hecate/services/BroadcastReceiverService.kt
+++ b/app/src/main/java/dev/lexip/hecate/services/BroadcastReceiverService.kt
@@ -25,7 +25,6 @@ import android.util.Log
import androidx.core.app.NotificationCompat
import dev.lexip.hecate.HecateApplication
import dev.lexip.hecate.R
-import dev.lexip.hecate.analytics.AnalyticsLogger
import dev.lexip.hecate.broadcasts.ScreenOnReceiver
import dev.lexip.hecate.data.UserPreferencesRepository
import dev.lexip.hecate.util.DarkThemeHandler
@@ -39,7 +38,7 @@ import kotlinx.coroutines.launch
private const val TAG = "BroadcastReceiverService"
private const val NOTIFICATION_CHANNEL_ID = "ForegroundServiceChannel"
-private const val ACTION_STOP_SERVICE = "dev.lexip.hecate.action.STOP_SERVICE"
+private const val ACTION_PAUSE_SERVICE = "dev.lexip.hecate.action.STOP_SERVICE"
private var screenOnReceiver: ScreenOnReceiver? = null
@@ -67,23 +66,10 @@ class BroadcastReceiverService : Service() {
val dataStore = (this.applicationContext as HecateApplication).userPreferencesDataStore
// Handle stop action from notification
- if (intent?.action == ACTION_STOP_SERVICE) {
- Log.i(
- TAG,
- "Disable action received from notification - disabling adaptive theme and stopping service..."
- )
+ if (intent?.action == ACTION_PAUSE_SERVICE) {
+ Log.i(TAG, "Pause action received from notification.")
serviceScope.launch {
- try {
- val userPreferencesRepository = UserPreferencesRepository(dataStore)
- userPreferencesRepository.updateAdaptiveThemeEnabled(false)
- Log.i(TAG, "Adaptive theme disabled via notification action.")
- AnalyticsLogger.logServiceDisabled(
- applicationContext,
- source = "notification_action"
- )
- } catch (e: Exception) {
- Log.e(TAG, "Failed to update adaptive theme preference", e)
- }
+ Log.i(TAG, "Adaptive theme paused/killed via notification action.")
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
@@ -161,22 +147,6 @@ class BroadcastReceiverService : Service() {
pendingIntent
).build()
- // Create action to stop the service
- val stopIntent = Intent(this, BroadcastReceiverService::class.java).apply {
- action = ACTION_STOP_SERVICE
- }
- val stopPendingIntent = PendingIntent.getService(
- this,
- 0,
- stopIntent,
- PendingIntent.FLAG_IMMUTABLE
- )
- val stopAction = NotificationCompat.Action.Builder(
- 0,
- getString(R.string.action_stop_service),
- stopPendingIntent
- ).build()
-
// Build notification
val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
@@ -186,7 +156,6 @@ class BroadcastReceiverService : Service() {
.setOnlyAlertOnce(true)
.setContentIntent(pendingIntent)
.addAction(disableAction)
- .addAction(stopAction)
.setOngoing(true)
diff --git a/app/src/main/java/dev/lexip/hecate/ui/AdaptiveThemeScreen.kt b/app/src/main/java/dev/lexip/hecate/ui/AdaptiveThemeScreen.kt
index 38c7b60..5f0ad66 100644
--- a/app/src/main/java/dev/lexip/hecate/ui/AdaptiveThemeScreen.kt
+++ b/app/src/main/java/dev/lexip/hecate/ui/AdaptiveThemeScreen.kt
@@ -13,26 +13,21 @@
package dev.lexip.hecate.ui
import android.Manifest
-import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
-import android.content.Intent
import android.content.pm.PackageManager
-import android.provider.Settings
-import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.MoreVert
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -45,50 +40,30 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
-import androidx.core.net.toUri
import androidx.lifecycle.viewmodel.compose.viewModel
-import dev.lexip.hecate.BuildConfig
import dev.lexip.hecate.R
-import dev.lexip.hecate.analytics.AnalyticsLogger
import dev.lexip.hecate.data.AdaptiveThreshold
import dev.lexip.hecate.ui.components.MainSwitchPreferenceCard
+import dev.lexip.hecate.ui.components.ThreeDotMenu
import dev.lexip.hecate.ui.components.preferences.CustomThresholdDialog
import dev.lexip.hecate.ui.components.preferences.ProgressDetailCard
import dev.lexip.hecate.ui.components.preferences.SliderDetailCard
-import dev.lexip.hecate.ui.setup.PermissionSetupWizardScreen
-import dev.lexip.hecate.ui.setup.PermissionWizardStep
+import dev.lexip.hecate.ui.setup.PermissionSetupHost
import dev.lexip.hecate.ui.theme.hecateTopAppBarColors
-import java.net.URLEncoder
-import java.nio.charset.StandardCharsets
-
-// Helper to share via Android Sharesheet
-private fun android.content.Context.shareSetupUrl(url: String) {
- if (url.isBlank()) return
-
- val sendIntent = Intent().apply {
- action = Intent.ACTION_SEND
- putExtra(Intent.EXTRA_TEXT, url)
- putExtra(Intent.EXTRA_TITLE, "Setup - Adaptive Theme")
- type = "text/plain"
- }
-
- val shareIntent = Intent.createChooser(sendIntent, null)
- startActivity(shareIntent)
-
-}
private val ScreenHorizontalMargin = 20.dp
+private val horizontalOffsetPadding = 8.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -98,15 +73,13 @@ fun AdaptiveThemeScreen(
) {
// Enable top-app-bar collapsing on small devices
val windowInfo = LocalWindowInfo.current
- val density = androidx.compose.ui.platform.LocalDensity.current
+ val density = LocalDensity.current
val screenHeightDp = with(density) { windowInfo.containerSize.height.toDp().value }
val enableCollapsing = screenHeightDp < 700f
val scrollBehavior = if (enableCollapsing) {
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
} else null
- val horizontalOffsetPadding = 8.dp
-
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val packageName = context.packageName
@@ -151,7 +124,7 @@ fun AdaptiveThemeScreen(
LargeTopAppBar(
modifier = Modifier
.padding(start = ScreenHorizontalMargin - 8.dp)
- .padding(top = 22.dp, bottom = 12.dp),
+ .padding(top = 12.dp, bottom = 12.dp),
colors = hecateTopAppBarColors(),
title = {
Text(
@@ -164,131 +137,29 @@ fun AdaptiveThemeScreen(
)
},
actions = {
- stringResource(id = R.string.error_no_email_client)
- var menuExpanded by remember { mutableStateOf(false) }
- androidx.compose.foundation.layout.Box {
- IconButton(onClick = { menuExpanded = true }) {
- Icon(
- imageVector = Icons.Filled.MoreVert,
- contentDescription = stringResource(id = R.string.title_more)
- )
- }
- DropdownMenu(
- expanded = menuExpanded,
- onDismissRequest = { menuExpanded = false }
- ) {
- val feedbackSubject =
- "Adaptive Theme Feedback (v${BuildConfig.VERSION_NAME})"
-
- // 1) Custom Threshold
- DropdownMenuItem(
- text = { Text(text = stringResource(id = R.string.title_custom_threshold)) },
- enabled = uiState.adaptiveThemeEnabled,
- onClick = {
- menuExpanded = false
- AnalyticsLogger.logOverflowMenuItemClicked(
- context,
- "custom_threshold"
- )
- if (uiState.adaptiveThemeEnabled) {
- showCustomDialog.value = true
- }
- }
- )
-
- // 2) Change Language (Android 13+)
- if (android.os.Build.VERSION.SDK_INT >= 33) {
- DropdownMenuItem(
- text = { Text(text = stringResource(id = R.string.title_change_language)) },
- onClick = {
- menuExpanded = false
- AnalyticsLogger.logOverflowMenuItemClicked(
- context,
- "change_language"
- )
- val intent =
- Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply {
- data = "package:$packageName".toUri()
- }
- context.startActivity(intent)
- }
- )
- }
-
- // 3) Send Feedback
- DropdownMenuItem(
- text = { Text(text = stringResource(id = R.string.title_send_feedback)) },
- onClick = {
- menuExpanded = false
- AnalyticsLogger.logOverflowMenuItemClicked(
- context,
- "send_feedback"
- )
- val encodedSubject = URLEncoder.encode(
- feedbackSubject,
- StandardCharsets.UTF_8.toString()
- )
- val feedbackUri =
- "https://lexip.dev/hecate/feedback?subject=$encodedSubject".toUri()
- val feedbackIntent = Intent(Intent.ACTION_VIEW, feedbackUri)
- context.startActivity(feedbackIntent)
-
- }
- )
-
- // 3) Beta Feedback (only on beta builds)
- if (BuildConfig.VERSION_NAME.contains("-beta")) {
- DropdownMenuItem(
- text = { Text(text = "Beta Feedback") },
- onClick = {
- menuExpanded = false
- AnalyticsLogger.logOverflowMenuItemClicked(
- context,
- "beta_feedback"
- )
- val betaUri =
- "https://play.google.com/store/apps/details?id=dev.lexip.hecate".toUri()
- val betaIntent = Intent(Intent.ACTION_VIEW, betaUri)
- context.startActivity(betaIntent)
- }
- )
- }
-
- // 4) About
- DropdownMenuItem(
- text = { Text(stringResource(R.string.title_about)) },
- onClick = {
- menuExpanded = false
- AnalyticsLogger.logOverflowMenuItemClicked(context, "about")
- val aboutUri = "https://lexip.dev/hecate/about".toUri()
- val aboutIntent = Intent(Intent.ACTION_VIEW, aboutUri)
- Toast.makeText(
- context,
- "v${BuildConfig.VERSION_NAME}",
- Toast.LENGTH_SHORT
- ).show()
- try {
- context.startActivity(aboutIntent)
- } catch (_: ActivityNotFoundException) {
- context.startActivity(Intent(Intent.ACTION_VIEW, aboutUri))
- }
- onAboutClick()
- }
- )
- }
- }
+ ThreeDotMenu(
+ isAdaptiveThemeEnabled = uiState.adaptiveThemeEnabled,
+ packageName = packageName,
+ onShowCustomThresholdDialog = { showCustomDialog.value = true },
+ onAboutClick = onAboutClick
+ )
},
scrollBehavior = scrollBehavior
)
}
) { innerPadding ->
+ val hasWriteSecureSettingsPermission = ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.WRITE_SECURE_SETTINGS
+ ) == PackageManager.PERMISSION_GRANTED
+
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = ScreenHorizontalMargin)
.verticalScroll(rememberScrollState()),
- verticalArrangement = Arrangement.spacedBy(32.dp)
+ verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Text(
@@ -296,17 +167,55 @@ fun AdaptiveThemeScreen(
text = stringResource(id = R.string.description_adaptive_theme),
style = MaterialTheme.typography.bodyLarge.copy(lineHeight = 21.sp)
)
+
+ // Setup card shown when the required permission has not been granted yet
+ if (!hasWriteSecureSettingsPermission) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
+ contentColor = MaterialTheme.colorScheme.onSurface
+ ),
+ shape = RoundedCornerShape(24.dp)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = stringResource(id = R.string.setup_required_title),
+ style = MaterialTheme.typography.titleMedium
+ )
+ Spacer(modifier = Modifier.padding(top = 4.dp))
+ Text(
+ text = stringResource(
+ id = R.string.setup_required_message,
+ stringResource(id = R.string.app_name)
+ ),
+ style = MaterialTheme.typography.bodyMedium
+ )
+ Spacer(modifier = Modifier.padding(top = 12.dp))
+ androidx.compose.foundation.layout.Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ androidx.compose.material3.Button(onClick = {
+ adaptiveThemeViewModel.onSetupRequested(packageName)
+ }) {
+ Text(text = stringResource(id = R.string.action_finish_setup))
+ }
+ }
+ }
+ }
+ }
+
MainSwitchPreferenceCard(
- text = stringResource(id = R.string.action_use_adaptive_theme),
+ text = stringResource(
+ id = R.string.action_use_adaptive_theme,
+ stringResource(id = R.string.app_name)
+ ),
isChecked = uiState.adaptiveThemeEnabled,
onCheckedChange = { checked ->
- val hasPermission = ContextCompat.checkSelfPermission(
- context, Manifest.permission.WRITE_SECURE_SETTINGS
- ) == PackageManager.PERMISSION_GRANTED
-
adaptiveThemeViewModel.onServiceToggleRequested(
checked,
- hasPermission,
+ hasWriteSecureSettingsPermission,
packageName
).also { wasToggled ->
if (wasToggled)
@@ -357,202 +266,38 @@ fun AdaptiveThemeScreen(
)
}
- }
- }
-
- // Show permission wizard if needed
- if (internalUiState.showPermissionWizard) {
- var isDeveloperOptionsEnabled by remember { mutableStateOf(false) }
- var isUsbDebuggingEnabled by remember { mutableStateOf(false) }
- var isUsbConnected by remember { mutableStateOf(false) }
- var hasPermission by remember { mutableStateOf(false) }
-
- // Periodically check developer settings and permission status
- LaunchedEffect(Unit) {
- var previousDevOptionsState = try {
- Settings.Global.getInt(
- context.contentResolver,
- Settings.Global.DEVELOPMENT_SETTINGS_ENABLED,
- 0
- ) == 1
- } catch (_: Exception) {
- false
- }
-
- var previousUsbDebuggingState = try {
- Settings.Global.getInt(
- context.contentResolver,
- Settings.Global.ADB_ENABLED,
- 0
- ) == 1
- } catch (_: Exception) {
- false
- }
-
-
- // Observe USB state via sticky broadcast and runtime receiver
- val usbFilter =
- android.content.IntentFilter("android.hardware.usb.action.USB_STATE")
- val sticky = context.registerReceiver(null, usbFilter)
- fun parseUsbIntent(intent: Intent?): Boolean {
- if (intent == null) return false
- val extras = intent.extras ?: return false
- val connected = extras.getBoolean("connected", false)
- val configured = extras.getBoolean("configured", false)
- val dataConnected = extras.getBoolean("data_connected", false)
- val adb = extras.getBoolean("adb", false)
- val hostConnected = extras.getBoolean("host_connected", false)
- return connected && (configured || dataConnected || adb || hostConnected)
- }
- isUsbConnected = parseUsbIntent(sticky)
- var previousUsbConnected = isUsbConnected
- val runtimeReceiver = object : android.content.BroadcastReceiver() {
- override fun onReceive(
- ctx: android.content.Context?,
- intent: Intent?
+ // Device-covered warning when the proximity sensor reports covered
+ if (internalUiState.isDeviceCovered && uiState.adaptiveThemeEnabled) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.onErrorContainer
+ ),
+ shape = RoundedCornerShape(20.dp)
) {
- val nowConnected = parseUsbIntent(intent)
- if (!previousUsbConnected && nowConnected) {
- haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
- }
- isUsbConnected = nowConnected
- previousUsbConnected = nowConnected
- }
- }
- context.registerReceiver(runtimeReceiver, usbFilter)
-
- try {
- // Fallback: check attached USB devices via UsbManager
- val usbManager =
- context.getSystemService(android.content.Context.USB_SERVICE) as? android.hardware.usb.UsbManager
- val nowConnected = (usbManager?.deviceList?.isNotEmpty() == true)
- if (!previousUsbConnected && nowConnected) {
- haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
- }
- isUsbConnected = isUsbConnected || nowConnected
- previousUsbConnected = isUsbConnected
- } catch (_: Exception) { /* ignore */
- }
-
- try {
- while (true) {
- isDeveloperOptionsEnabled = try {
- Settings.Global.getInt(
- context.contentResolver,
- Settings.Global.DEVELOPMENT_SETTINGS_ENABLED,
- 0
- ) == 1
- } catch (_: Exception) {
- false
- }
-
- isUsbDebuggingEnabled = try {
- Settings.Global.getInt(
- context.contentResolver,
- Settings.Global.ADB_ENABLED,
- 0
- ) == 1
- } catch (_: Exception) {
- false
- }
-
- hasPermission = ContextCompat.checkSelfPermission(
- context, Manifest.permission.WRITE_SECURE_SETTINGS
- ) == PackageManager.PERMISSION_GRANTED
-
- if (!previousDevOptionsState && isDeveloperOptionsEnabled) {
- haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
- }
-
- if (!previousUsbDebuggingState && isUsbDebuggingEnabled) {
- haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
- }
-
- previousDevOptionsState = isDeveloperOptionsEnabled
- previousUsbDebuggingState = isUsbDebuggingEnabled
-
- // Fallback refresh: if sticky broadcast wasn’t conclusive, re-check UsbManager
- if (!isUsbConnected) {
- val usbManager =
- context.getSystemService(android.content.Context.USB_SERVICE) as? android.hardware.usb.UsbManager
- val nowConnected = usbManager?.deviceList?.isNotEmpty() == true
- if (!previousUsbConnected && nowConnected) {
- haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
- }
- isUsbConnected = nowConnected
- previousUsbConnected = nowConnected
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = stringResource(id = R.string.device_covered_title),
+ style = MaterialTheme.typography.titleMedium
+ )
+ Spacer(modifier = Modifier.padding(top = 4.dp))
+ Text(
+ text = stringResource(id = R.string.device_covered_message),
+ style = MaterialTheme.typography.bodyMedium
+ )
}
-
- // If permission becomes granted, auto-complete wizard and enable service
- if (hasPermission) {
- haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
- adaptiveThemeViewModel.completePermissionWizardAndEnableService()
- break
- }
-
- // Check every second
- kotlinx.coroutines.delay(1000)
}
- } finally {
- context.unregisterReceiver(runtimeReceiver)
}
+ Spacer(modifier = Modifier.padding(bottom = 4.dp))
}
+ }
- val adbCommand by adaptiveThemeViewModel.pendingAdbCommand.collectAsState()
-
- PermissionSetupWizardScreen(
- step = internalUiState.permissionWizardStep,
- adbCommand = adbCommand,
- isUsbConnected = isUsbConnected,
- hasWriteSecureSettings = hasPermission,
- isDeveloperOptionsEnabled = isDeveloperOptionsEnabled,
- isUsbDebuggingEnabled = isUsbDebuggingEnabled,
- onNext = {
- haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
- if (internalUiState.permissionWizardStep == PermissionWizardStep.GRANT_PERMISSION && hasPermission) {
- adaptiveThemeViewModel.completePermissionWizardAndEnableService()
- } else {
- adaptiveThemeViewModel.goToNextPermissionWizardStep()
- }
- },
- onExit = { adaptiveThemeViewModel.dismissPermissionWizard() },
- onOpenSettings = {
- val intent = Intent(Settings.ACTION_DEVICE_INFO_SETTINGS)
- try {
- context.startActivity(intent)
- } catch (_: Exception) {
- context.startActivity(Intent(Settings.ACTION_SETTINGS))
- }
- },
- onOpenDeveloperSettings = {
- val intent = Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS)
- try {
- context.startActivity(intent)
- } catch (_: Exception) {
- context.startActivity(Intent(Settings.ACTION_SETTINGS))
- }
- },
- onShareSetupUrl = {
- AnalyticsLogger.logShareLinkClicked(context, "permission_wizard")
- context.shareSetupUrl("https://lexip.dev/setup")
- },
- onCopyAdbCommand = { adaptiveThemeViewModel.requestCopyAdbCommand() },
- onShareExpertCommand = {
- context.shareSetupUrl(adbCommand)
- },
- onCheckPermission = {
- val nowGranted =
- ContextCompat.checkSelfPermission(
- context, Manifest.permission.WRITE_SECURE_SETTINGS
- ) == PackageManager.PERMISSION_GRANTED
- adaptiveThemeViewModel.recheckWriteSecureSettingsPermission(nowGranted)
- if (nowGranted) {
- haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
- adaptiveThemeViewModel.completePermissionWizardAndEnableService()
- }
- }
- )
+ // Show permission wizard if needed
+ if (internalUiState.showPermissionWizard) {
+ PermissionSetupHost(viewModel = adaptiveThemeViewModel)
return
}
@@ -565,4 +310,4 @@ fun AdaptiveThemeScreen(
},
onDismiss = { showCustomDialog.value = false }
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/dev/lexip/hecate/ui/AdaptiveThemeViewModel.kt b/app/src/main/java/dev/lexip/hecate/ui/AdaptiveThemeViewModel.kt
index 67559d2..560808f 100644
--- a/app/src/main/java/dev/lexip/hecate/ui/AdaptiveThemeViewModel.kt
+++ b/app/src/main/java/dev/lexip/hecate/ui/AdaptiveThemeViewModel.kt
@@ -13,6 +13,7 @@
package dev.lexip.hecate.ui
import android.content.Intent
+import android.hardware.SensorManager
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@@ -25,6 +26,7 @@ import dev.lexip.hecate.services.BroadcastReceiverService
import dev.lexip.hecate.ui.setup.PermissionWizardStep
import dev.lexip.hecate.util.DarkThemeHandler
import dev.lexip.hecate.util.LightSensorManager
+import dev.lexip.hecate.util.ProximitySensorManager
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -45,7 +47,8 @@ data class AdaptiveThemeUiState(
val permissionWizardStep: PermissionWizardStep = PermissionWizardStep.ENABLE_DEVELOPER_MODE,
val permissionWizardCompleted: Boolean = false,
val hasAutoAdvancedFromDeveloperMode: Boolean = false,
- val hasAutoAdvancedFromConnectUsb: Boolean = false
+ val hasAutoAdvancedFromConnectUsb: Boolean = false,
+ val isDeviceCovered: Boolean = false
)
class AdaptiveThemeViewModel(
@@ -58,6 +61,8 @@ class AdaptiveThemeViewModel(
private val _uiState = MutableStateFlow(AdaptiveThemeUiState())
val uiState: StateFlow = _uiState.asStateFlow()
+ fun isAdaptiveThemeEnabled(): Boolean = _uiState.value.adaptiveThemeEnabled
+
// One-shot UI events
private val _uiEvents = MutableSharedFlow(
replay = 0,
@@ -82,21 +87,69 @@ class AdaptiveThemeViewModel(
_currentSensorLux.value = lux
}
+ // Proximity Sensor
+ private val proximitySensorManager = ProximitySensorManager(application.applicationContext)
+ private var isListeningToProximity = false
+
+ fun onSetupRequested(packageName: String) {
+ onServiceToggleRequested(
+ checked = true,
+ hasPermission = false,
+ packageName = packageName
+ )
+ }
+
+ private fun startProximityListening() {
+ if (isListeningToProximity) return
+ isListeningToProximity = true
+ proximitySensorManager.startListening({ distance: Float ->
+ val covered = distance < 5f
+ if (covered != _uiState.value.isDeviceCovered) {
+ if (covered) Thread.sleep(1000) // Prevents UI flickering
+ _uiState.value = _uiState.value.copy(isDeviceCovered = covered)
+ }
+ }, sensorDelay = SensorManager.SENSOR_DELAY_UI)
+ }
+
+ private fun stopProximityListening() {
+ if (!isListeningToProximity) return
+ isListeningToProximity = false
+ proximitySensorManager.stopListening()
+ if (_uiState.value.isDeviceCovered) {
+ _uiState.value = _uiState.value.copy(isDeviceCovered = false)
+ }
+ }
+
+ fun startSensorsIfEnabled() {
+ if (_uiState.value.adaptiveThemeEnabled) {
+ startLightSensorListening()
+ startProximityListening()
+ }
+ }
+
+ fun stopSensors() {
+ stopLightSensorListening()
+ stopProximityListening()
+ }
+
// Temporary variable for custom threshold
private var customThresholdTemp: Float? = null
init {
viewModelScope.launch {
userPreferencesRepository.userPreferencesFlow.collect { userPreferences ->
- _uiState.value = AdaptiveThemeUiState(
+ _uiState.value = _uiState.value.copy(
adaptiveThemeEnabled = userPreferences.adaptiveThemeEnabled,
adaptiveThemeThresholdLux = userPreferences.adaptiveThemeThresholdLux,
customAdaptiveThemeThresholdLux = userPreferences.customAdaptiveThemeThresholdLux,
permissionWizardCompleted = userPreferences.permissionWizardCompleted
)
- if (userPreferences.adaptiveThemeEnabled) startLightSensorListening()
- else stopLightSensorListening()
+ if (userPreferences.adaptiveThemeEnabled) {
+ startSensorsIfEnabled()
+ } else {
+ stopSensors()
+ }
}
}
}
@@ -104,11 +157,11 @@ class AdaptiveThemeViewModel(
private fun startLightSensorListening() {
if (isListeningToSensor) return
isListeningToSensor = true
- lightSensorManager.startListening { lux ->
+ lightSensorManager.startListening({ lux: Float ->
viewModelScope.launch {
updateCurrentSensorLux(lux)
}
- }
+ }, sensorDelay = SensorManager.SENSOR_DELAY_UI)
}
private fun stopLightSensorListening() {
@@ -119,6 +172,7 @@ class AdaptiveThemeViewModel(
override fun onCleared() {
stopLightSensorListening()
+ stopProximityListening()
super.onCleared()
}
diff --git a/app/src/main/java/dev/lexip/hecate/ui/InAppUpdateManager.kt b/app/src/main/java/dev/lexip/hecate/ui/InAppUpdateManager.kt
index 148c8a8..616cc6f 100644
--- a/app/src/main/java/dev/lexip/hecate/ui/InAppUpdateManager.kt
+++ b/app/src/main/java/dev/lexip/hecate/ui/InAppUpdateManager.kt
@@ -24,25 +24,36 @@ import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.appupdate.AppUpdateOptions
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.UpdateAvailability
+import dev.lexip.hecate.analytics.AnalyticsGate
+import dev.lexip.hecate.analytics.AnalyticsLogger
private const val TAG = "InAppUpdateManager"
-private const val DAYS_FOR_IMMEDIATE_UPDATE = 0
+private const val DAYS_FOR_IMMEDIATE_UPDATE = 3
private const val MIN_PRIORITY_FOR_IMMEDIATE = 0
+private const val DAYS_FOR_FLEXIBLE_UPDATE = 1
+private const val MIN_PRIORITY_FOR_FLEXIBLE = 0
class InAppUpdateManager(activity: ComponentActivity) {
- private val appUpdateManager: AppUpdateManager = AppUpdateManagerFactory.create(activity)
+ private val appUpdateManager: AppUpdateManager? = if (AnalyticsGate.isPlayStoreInstall()) {
+ AppUpdateManagerFactory.create(activity)
+ } else null
private var updateLauncher: ActivityResultLauncher? = null
fun registerUpdateLauncher(activity: ComponentActivity) {
if (updateLauncher != null) return
+ if (!AnalyticsGate.isPlayStoreInstall()) {
+ return
+ }
+ appUpdateManager ?: return
updateLauncher =
activity.registerForActivityResult(StartIntentSenderForResult()) { result ->
when (result.resultCode) {
Activity.RESULT_OK -> {
- Log.d(TAG, "In-app update completed successfully")
+ Log.i(TAG, "In-app update completed successfully")
+ AnalyticsLogger.logInAppUpdateInstalled(activity)
}
Activity.RESULT_CANCELED -> {
@@ -70,29 +81,28 @@ class InAppUpdateManager(activity: ComponentActivity) {
onNoUpdate: () -> Unit = {},
onError: (Throwable) -> Unit = {}
) {
+ if (!AnalyticsGate.isPlayStoreInstall()) {
+ return
+ }
val launcher = updateLauncher
if (launcher == null) {
Log.w(TAG, "checkForImmediateUpdate called before launcher was registered")
return
}
+ val manager = appUpdateManager ?: return
- appUpdateManager.appUpdateInfo
+ manager.appUpdateInfo
.addOnSuccessListener { appUpdateInfo ->
val availability = appUpdateInfo.updateAvailability()
val isImmediateAllowed = appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
val staleness = appUpdateInfo.clientVersionStalenessDays() ?: -1
val priority = appUpdateInfo.updatePriority()
- Log.d(
- TAG,
- "Update info: availability=$availability, immediateAllowed=$isImmediateAllowed, stalenessDays=$staleness, priority=$priority"
- )
-
val meetsStaleness = staleness == -1 || staleness >= DAYS_FOR_IMMEDIATE_UPDATE
val meetsPriority = priority >= MIN_PRIORITY_FOR_IMMEDIATE
if (availability == UpdateAvailability.UPDATE_AVAILABLE && isImmediateAllowed && meetsStaleness && meetsPriority) {
- Log.d(TAG, "Immediate update available, starting update flow")
+ Log.i(TAG, "Immediate in-app update: starting update flow")
try {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
@@ -104,10 +114,6 @@ class InAppUpdateManager(activity: ComponentActivity) {
onError(t)
}
} else {
- Log.d(
- TAG,
- "No eligible immediate update. availability=$availability, immediateAllowed=$isImmediateAllowed"
- )
onNoUpdate()
}
}
@@ -117,19 +123,69 @@ class InAppUpdateManager(activity: ComponentActivity) {
}
}
+ fun checkForFlexibleUpdate(
+ onNoUpdate: () -> Unit = {},
+ onError: (Throwable) -> Unit = {}
+ ) {
+ if (!AnalyticsGate.isPlayStoreInstall()) {
+ return
+ }
+ val launcher = updateLauncher
+ if (launcher == null) {
+ Log.w(TAG, "checkForFlexibleUpdate called before launcher was registered")
+ return
+ }
+ val manager = appUpdateManager ?: return
+
+ manager.appUpdateInfo
+ .addOnSuccessListener { appUpdateInfo ->
+ val availability = appUpdateInfo.updateAvailability()
+ val isFlexibleAllowed = appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
+ val staleness = appUpdateInfo.clientVersionStalenessDays() ?: -1
+ val priority = appUpdateInfo.updatePriority()
+
+ val meetsStaleness = staleness == -1 || staleness >= DAYS_FOR_FLEXIBLE_UPDATE
+ val meetsPriority = priority >= MIN_PRIORITY_FOR_FLEXIBLE
+
+ if (availability == UpdateAvailability.UPDATE_AVAILABLE && isFlexibleAllowed && meetsStaleness && meetsPriority) {
+ Log.i(TAG, "Flexible in-app update: starting update flow")
+ try {
+ appUpdateManager.startUpdateFlowForResult(
+ appUpdateInfo,
+ launcher,
+ AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build()
+ )
+ } catch (t: Throwable) {
+ Log.e(TAG, "Failed to launch flexible in-app update", t)
+ onError(t)
+ }
+ } else {
+ onNoUpdate()
+ }
+ }
+ .addOnFailureListener { throwable ->
+ Log.e(TAG, "Failed to retrieve appUpdateInfo for flexible update", throwable)
+ onError(throwable)
+ }
+ }
+
fun resumeImmediateUpdateIfNeeded() {
+ if (!AnalyticsGate.isPlayStoreInstall()) {
+ return
+ }
val launcher = updateLauncher
if (launcher == null) {
Log.w(TAG, "resumeImmediateUpdateIfNeeded called before launcher was registered")
return
}
+ val manager = appUpdateManager ?: return
- appUpdateManager.appUpdateInfo
+ manager.appUpdateInfo
.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS &&
appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
) {
- Log.d(TAG, "Resuming in-progress immediate in-app update")
+ Log.i(TAG, "Resuming in-progress immediate in-app update")
try {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
@@ -139,15 +195,43 @@ class InAppUpdateManager(activity: ComponentActivity) {
} catch (t: Throwable) {
Log.e(TAG, "Failed to resume immediate in-app update", t)
}
- } else {
- Log.d(
- TAG,
- "No in-progress immediate update to resume. availability=${appUpdateInfo.updateAvailability()}"
- )
}
}
.addOnFailureListener { throwable ->
Log.e(TAG, "Failed to check for in-progress immediate update", throwable)
}
}
+
+ fun resumeFlexibleUpdateIfNeeded() {
+ if (!AnalyticsGate.isPlayStoreInstall()) {
+ return
+ }
+ val launcher = updateLauncher
+ if (launcher == null) {
+ Log.w(TAG, "resumeFlexibleUpdateIfNeeded called before launcher was registered")
+ return
+ }
+ val manager = appUpdateManager ?: return
+
+ manager.appUpdateInfo
+ .addOnSuccessListener { appUpdateInfo ->
+ if (appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS &&
+ appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
+ ) {
+ Log.i(TAG, "Resuming in-progress flexible in-app update")
+ try {
+ appUpdateManager.startUpdateFlowForResult(
+ appUpdateInfo,
+ launcher,
+ AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build()
+ )
+ } catch (t: Throwable) {
+ Log.e(TAG, "Failed to resume flexible in-app update", t)
+ }
+ }
+ }
+ .addOnFailureListener { throwable ->
+ Log.e(TAG, "Failed to check for in-progress flexible update", throwable)
+ }
+ }
}
diff --git a/app/src/main/java/dev/lexip/hecate/ui/MainActivity.kt b/app/src/main/java/dev/lexip/hecate/ui/MainActivity.kt
index bba79de..a86dc7e 100644
--- a/app/src/main/java/dev/lexip/hecate/ui/MainActivity.kt
+++ b/app/src/main/java/dev/lexip/hecate/ui/MainActivity.kt
@@ -22,18 +22,22 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import androidx.lifecycle.viewmodel.compose.viewModel
import dev.lexip.hecate.HecateApplication
import dev.lexip.hecate.data.UserPreferencesRepository
+import dev.lexip.hecate.services.BroadcastReceiverService
import dev.lexip.hecate.ui.theme.HecateTheme
import dev.lexip.hecate.util.DarkThemeHandler
+import dev.lexip.hecate.util.InstallSourceChecker
class MainActivity : ComponentActivity() {
- private lateinit var inAppUpdateManager: InAppUpdateManager
+ private var inAppUpdateManager: InAppUpdateManager? = null
+ private lateinit var adaptiveThemeViewModel: AdaptiveThemeViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ installSplashScreen()
+ enableEdgeToEdge()
// Catch mysterious unsupported SDK versions despite minSDK 31
@SuppressLint("ObsoleteSdkInt")
@@ -47,21 +51,26 @@ class MainActivity : ComponentActivity() {
return
}
- installSplashScreen()
- enableEdgeToEdge()
- inAppUpdateManager = InAppUpdateManager(this)
- inAppUpdateManager.registerUpdateLauncher(this)
+ val isPlayInstall = InstallSourceChecker.isInstalledFromPlayStore(this)
+ if (isPlayInstall) {
+ inAppUpdateManager = InAppUpdateManager(this).also { manager ->
+ manager.registerUpdateLauncher(this)
+ }
+ }
- setContent {
- val dataStore = (this.applicationContext as HecateApplication).userPreferencesDataStore
- val adaptiveThemeViewModel: AdaptiveThemeViewModel = viewModel(
- factory = AdaptiveThemeViewModelFactory(
- this.application as HecateApplication,
- UserPreferencesRepository(dataStore),
- DarkThemeHandler(applicationContext)
- )
+ // Obtain a stable ViewModel instance
+ val dataStore = (this.applicationContext as HecateApplication).userPreferencesDataStore
+ adaptiveThemeViewModel = androidx.lifecycle.ViewModelProvider(
+ this,
+ AdaptiveThemeViewModelFactory(
+ this.application as HecateApplication,
+ UserPreferencesRepository(dataStore),
+ DarkThemeHandler(applicationContext)
)
+ )[AdaptiveThemeViewModel::class.java]
+
+ setContent {
val state by adaptiveThemeViewModel.uiState.collectAsState()
HecateTheme {
@@ -71,13 +80,30 @@ class MainActivity : ComponentActivity() {
}
}
- inAppUpdateManager.checkForImmediateUpdate()
+ inAppUpdateManager?.checkForImmediateUpdate()
+ inAppUpdateManager?.checkForFlexibleUpdate()
}
override fun onResume() {
super.onResume()
- if (::inAppUpdateManager.isInitialized) {
- inAppUpdateManager.resumeImmediateUpdateIfNeeded()
+
+ inAppUpdateManager?.resumeImmediateUpdateIfNeeded()
+ inAppUpdateManager?.resumeFlexibleUpdateIfNeeded()
+
+ // Always restart the service (it may have been paused in the meantime)
+ if (this::adaptiveThemeViewModel.isInitialized) {
+ adaptiveThemeViewModel.startSensorsIfEnabled()
+ if (adaptiveThemeViewModel.isAdaptiveThemeEnabled()) {
+ val intent = android.content.Intent(this, BroadcastReceiverService::class.java)
+ androidx.core.content.ContextCompat.startForegroundService(this, intent)
+ }
+ }
+ }
+
+ override fun onPause() {
+ if (this::adaptiveThemeViewModel.isInitialized) {
+ adaptiveThemeViewModel.stopSensors()
}
+ super.onPause()
}
}
diff --git a/app/src/main/java/dev/lexip/hecate/ui/components/ThreeDotMenu.kt b/app/src/main/java/dev/lexip/hecate/ui/components/ThreeDotMenu.kt
new file mode 100644
index 0000000..9125bac
--- /dev/null
+++ b/app/src/main/java/dev/lexip/hecate/ui/components/ThreeDotMenu.kt
@@ -0,0 +1,170 @@
+package dev.lexip.hecate.ui.components
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.provider.Settings
+import android.widget.Toast
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.core.net.toUri
+import dev.lexip.hecate.BuildConfig
+import dev.lexip.hecate.R
+import dev.lexip.hecate.analytics.AnalyticsGate
+import dev.lexip.hecate.analytics.AnalyticsLogger
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
+
+
+const val FEEDBACK_SUBJECT = "Adaptive Theme Feedback (v${BuildConfig.VERSION_NAME})"
+
+@Composable
+fun ThreeDotMenu(
+ isAdaptiveThemeEnabled: Boolean,
+ packageName: String,
+ onShowCustomThresholdDialog: () -> Unit,
+ onAboutClick: () -> Unit = {}
+) {
+ val context = LocalContext.current
+ var menuExpanded by remember { mutableStateOf(false) }
+
+ androidx.compose.foundation.layout.Box {
+ IconButton(onClick = { menuExpanded = true }) {
+ Icon(
+ imageVector = Icons.Filled.MoreVert,
+ contentDescription = stringResource(id = R.string.title_more)
+ )
+ }
+ DropdownMenu(
+ expanded = menuExpanded,
+ onDismissRequest = { menuExpanded = false }
+ ) {
+
+ // 1) Custom Threshold
+ DropdownMenuItem(
+ text = { Text(text = stringResource(id = R.string.title_custom_threshold)) },
+ enabled = isAdaptiveThemeEnabled,
+ onClick = {
+ menuExpanded = false
+ AnalyticsLogger.logOverflowMenuItemClicked(
+ context,
+ "custom_threshold"
+ )
+ if (isAdaptiveThemeEnabled) {
+ onShowCustomThresholdDialog()
+ }
+ }
+ )
+
+ // 2) Change Language (Android 13+)
+ if (android.os.Build.VERSION.SDK_INT >= 33) {
+ DropdownMenuItem(
+ text = { Text(text = stringResource(id = R.string.title_change_language)) },
+ onClick = {
+ menuExpanded = false
+ AnalyticsLogger.logOverflowMenuItemClicked(
+ context,
+ "change_language"
+ )
+ val intent =
+ Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply {
+ data = "package:$packageName".toUri()
+ }
+ context.startActivity(intent)
+ }
+ )
+ }
+
+ // 3) Send Feedback
+ DropdownMenuItem(
+ text = { Text(text = stringResource(id = R.string.title_send_feedback)) },
+ onClick = {
+ menuExpanded = false
+ AnalyticsLogger.logOverflowMenuItemClicked(
+ context,
+ "send_feedback"
+ )
+ val encodedSubject = URLEncoder.encode(
+ FEEDBACK_SUBJECT,
+ StandardCharsets.UTF_8.toString()
+ )
+ val feedbackUri =
+ "https://lexip.dev/hecate/feedback?subject=$encodedSubject".toUri()
+ val feedbackIntent = Intent(Intent.ACTION_VIEW, feedbackUri)
+ context.startActivity(feedbackIntent)
+
+ }
+ )
+
+ // 4) Beta Feedback (only on beta builds)
+ if (BuildConfig.VERSION_NAME.contains("-beta") && AnalyticsGate.isPlayStoreInstall()) {
+ DropdownMenuItem(
+ text = { Text(text = "Beta Feedback") },
+ onClick = {
+ menuExpanded = false
+ AnalyticsLogger.logOverflowMenuItemClicked(
+ context,
+ "beta_feedback"
+ )
+ val betaUri =
+ "https://play.google.com/store/apps/details?id=dev.lexip.hecate".toUri()
+ val betaIntent = Intent(Intent.ACTION_VIEW, betaUri)
+ context.startActivity(betaIntent)
+ }
+ )
+ }
+
+ // 5) Star on GitHub
+ DropdownMenuItem(
+ text = { Text(text = stringResource(id = R.string.action_star_on_github)) },
+ onClick = {
+ menuExpanded = false
+ AnalyticsLogger.logOverflowMenuItemClicked(
+ context,
+ "star_github"
+ )
+ val starUri = "https://lexip.dev/hecate/source".toUri()
+ val starIntent = Intent(Intent.ACTION_VIEW, starUri)
+ try {
+ context.startActivity(starIntent)
+ } catch (_: ActivityNotFoundException) {
+ context.startActivity(Intent(Intent.ACTION_VIEW, starUri))
+ }
+ }
+ )
+
+ // 6) About
+ DropdownMenuItem(
+ text = { Text(stringResource(R.string.title_about)) },
+ onClick = {
+ menuExpanded = false
+ AnalyticsLogger.logOverflowMenuItemClicked(context, "about")
+ val aboutUri = "https://lexip.dev/hecate/about".toUri()
+ val aboutIntent = Intent(Intent.ACTION_VIEW, aboutUri)
+ Toast.makeText(
+ context,
+ "v${BuildConfig.VERSION_NAME}",
+ Toast.LENGTH_SHORT
+ ).show()
+ try {
+ context.startActivity(aboutIntent)
+ } catch (_: ActivityNotFoundException) {
+ context.startActivity(Intent(Intent.ACTION_VIEW, aboutUri))
+ }
+ onAboutClick()
+ }
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/dev/lexip/hecate/ui/components/preferences/ProgressDetailCard.kt b/app/src/main/java/dev/lexip/hecate/ui/components/preferences/ProgressDetailCard.kt
index 8ff14df..f32a54e 100644
--- a/app/src/main/java/dev/lexip/hecate/ui/components/preferences/ProgressDetailCard.kt
+++ b/app/src/main/java/dev/lexip/hecate/ui/components/preferences/ProgressDetailCard.kt
@@ -21,10 +21,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
@@ -63,6 +66,28 @@ fun ProgressDetailCard(
activeIndex = activeIndex,
enabled = enabled
)
+
+ // Show live lux measurement
+ if (enabled) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = "Live Measurement",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.weight(1f)
+ )
+ val liveLuxRounded = currentLux.toInt()
+ Text(
+ text = "$liveLuxRounded lx",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.End
+ )
+ }
+ }
}
}
}
diff --git a/app/src/main/java/dev/lexip/hecate/ui/setup/PermissionSetupHost.kt b/app/src/main/java/dev/lexip/hecate/ui/setup/PermissionSetupHost.kt
new file mode 100644
index 0000000..b3568e5
--- /dev/null
+++ b/app/src/main/java/dev/lexip/hecate/ui/setup/PermissionSetupHost.kt
@@ -0,0 +1,244 @@
+package dev.lexip.hecate.ui.setup
+
+import android.Manifest
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.provider.Settings
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.core.content.ContextCompat
+import dev.lexip.hecate.analytics.AnalyticsLogger
+import dev.lexip.hecate.ui.AdaptiveThemeViewModel
+
+@Composable
+fun PermissionSetupHost(
+ viewModel: AdaptiveThemeViewModel,
+) {
+ val context = LocalContext.current
+ val haptic = LocalHapticFeedback.current
+ val internalUiState by viewModel.uiState.collectAsState()
+
+ // Track setup-related environment state locally in this host
+ var isDeveloperOptionsEnabled by remember { mutableStateOf(false) }
+ var isUsbDebuggingEnabled by remember { mutableStateOf(false) }
+ var isUsbConnected by remember { mutableStateOf(false) }
+ var hasPermission by remember { mutableStateOf(false) }
+
+ SideEffect {
+ AnalyticsLogger.logSetupStarted(context)
+ }
+
+ // Periodically check developer settings, USB and permission status
+ LaunchedEffect(Unit) {
+ var previousDevOptionsState = try {
+ Settings.Global.getInt(
+ context.contentResolver,
+ Settings.Global.DEVELOPMENT_SETTINGS_ENABLED,
+ 0
+ ) == 1
+ } catch (_: Exception) {
+ false
+ }
+
+ var previousUsbDebuggingState = try {
+ Settings.Global.getInt(
+ context.contentResolver,
+ Settings.Global.ADB_ENABLED,
+ 0
+ ) == 1
+ } catch (_: Exception) {
+ false
+ }
+
+ // Observe USB state via sticky broadcast and runtime receiver
+ val usbFilter =
+ android.content.IntentFilter("android.hardware.usb.action.USB_STATE")
+ val sticky = context.registerReceiver(null, usbFilter)
+ fun parseUsbIntent(intent: Intent?): Boolean {
+ if (intent == null) return false
+ val extras = intent.extras ?: return false
+ val connected = extras.getBoolean("connected", false)
+ val configured = extras.getBoolean("configured", false)
+ val dataConnected = extras.getBoolean("data_connected", false)
+ val adb = extras.getBoolean("adb", false)
+ val hostConnected = extras.getBoolean("host_connected", false)
+ return connected && (configured || dataConnected || adb || hostConnected)
+ }
+ isUsbConnected = parseUsbIntent(sticky)
+ var previousUsbConnected = isUsbConnected
+
+ val runtimeReceiver = object : android.content.BroadcastReceiver() {
+ override fun onReceive(
+ ctx: android.content.Context?,
+ intent: Intent?
+ ) {
+ val nowConnected = parseUsbIntent(intent)
+ if (!previousUsbConnected && nowConnected) {
+ haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
+ }
+ isUsbConnected = nowConnected
+ previousUsbConnected = nowConnected
+ }
+ }
+ context.registerReceiver(runtimeReceiver, usbFilter)
+
+ try {
+ // Fallback: check attached USB devices via UsbManager
+ val usbManager =
+ context.getSystemService(android.content.Context.USB_SERVICE) as? android.hardware.usb.UsbManager
+ val nowConnected = (usbManager?.deviceList?.isNotEmpty() == true)
+ if (!previousUsbConnected && nowConnected) {
+ haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
+ }
+ isUsbConnected = isUsbConnected || nowConnected
+ previousUsbConnected = isUsbConnected
+ } catch (_: Exception) {
+ // ignore
+ }
+
+ try {
+ while (true) {
+ isDeveloperOptionsEnabled = try {
+ Settings.Global.getInt(
+ context.contentResolver,
+ Settings.Global.DEVELOPMENT_SETTINGS_ENABLED,
+ 0
+ ) == 1
+ } catch (_: Exception) {
+ false
+ }
+
+ isUsbDebuggingEnabled = try {
+ Settings.Global.getInt(
+ context.contentResolver,
+ Settings.Global.ADB_ENABLED,
+ 0
+ ) == 1
+ } catch (_: Exception) {
+ false
+ }
+
+ hasPermission = ContextCompat.checkSelfPermission(
+ context, Manifest.permission.WRITE_SECURE_SETTINGS
+ ) == PackageManager.PERMISSION_GRANTED
+
+ if (!previousDevOptionsState && isDeveloperOptionsEnabled) {
+ haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
+ }
+
+ if (!previousUsbDebuggingState && isUsbDebuggingEnabled) {
+ haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
+ }
+
+ previousDevOptionsState = isDeveloperOptionsEnabled
+ previousUsbDebuggingState = isUsbDebuggingEnabled
+
+ // Fallback refresh: if sticky broadcast wasn’t conclusive, re-check UsbManager
+ if (!isUsbConnected) {
+ val usbManager =
+ context.getSystemService(android.content.Context.USB_SERVICE) as? android.hardware.usb.UsbManager
+ val nowConnected = usbManager?.deviceList?.isNotEmpty() == true
+ if (!previousUsbConnected && nowConnected) {
+ haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
+ }
+ isUsbConnected = nowConnected
+ previousUsbConnected = nowConnected
+ }
+
+ // If permission becomes granted, auto-complete wizard and enable service
+ if (hasPermission) {
+ haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
+ viewModel.completePermissionWizardAndEnableService()
+ break
+ }
+
+ // Check every second
+ kotlinx.coroutines.delay(1000)
+ }
+ } finally {
+ context.unregisterReceiver(runtimeReceiver)
+ }
+ }
+
+ val adbCommand by viewModel.pendingAdbCommand.collectAsState()
+
+ PermissionSetupWizardScreen(
+ step = internalUiState.permissionWizardStep,
+ adbCommand = adbCommand,
+ isUsbConnected = isUsbConnected,
+ hasWriteSecureSettings = hasPermission,
+ isDeveloperOptionsEnabled = isDeveloperOptionsEnabled,
+ isUsbDebuggingEnabled = isUsbDebuggingEnabled,
+ onNext = {
+ haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
+ when (internalUiState.permissionWizardStep) {
+ PermissionWizardStep.ENABLE_DEVELOPER_MODE -> {
+ AnalyticsLogger.logSetupStepOneCompleted(context)
+ viewModel.goToNextPermissionWizardStep()
+ }
+
+ PermissionWizardStep.CONNECT_USB -> {
+ AnalyticsLogger.logSetupStepTwoCompleted(context)
+ viewModel.goToNextPermissionWizardStep()
+ }
+
+ PermissionWizardStep.GRANT_PERMISSION -> {
+ if (hasPermission) {
+ AnalyticsLogger.logSetupFinished(context)
+ viewModel.completePermissionWizardAndEnableService()
+ } else {
+ viewModel.goToNextPermissionWizardStep()
+ }
+ }
+ }
+ },
+ onExit = {
+ viewModel.dismissPermissionWizard()
+ },
+ onOpenSettings = {
+ val intent = Intent(Settings.ACTION_DEVICE_INFO_SETTINGS)
+ try {
+ context.startActivity(intent)
+ } catch (_: Exception) {
+ context.startActivity(Intent(Settings.ACTION_SETTINGS))
+ }
+ },
+ onOpenDeveloperSettings = {
+ val intent = Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS)
+ try {
+ context.startActivity(intent)
+ } catch (_: Exception) {
+ context.startActivity(Intent(Settings.ACTION_SETTINGS))
+ }
+ },
+ onShareSetupUrl = {
+ AnalyticsLogger.logShareLinkClicked(context, "permission_wizard")
+ context.shareSetupUrl("https://lexip.dev/setup")
+ },
+ onCopyAdbCommand = { viewModel.requestCopyAdbCommand() },
+ onShareExpertCommand = {
+ context.shareSetupUrl(adbCommand)
+ },
+ onCheckPermission = {
+ val nowGranted =
+ ContextCompat.checkSelfPermission(
+ context, Manifest.permission.WRITE_SECURE_SETTINGS
+ ) == PackageManager.PERMISSION_GRANTED
+ viewModel.recheckWriteSecureSettingsPermission(nowGranted)
+ if (nowGranted) {
+ haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
+ AnalyticsLogger.logSetupFinished(context)
+ viewModel.completePermissionWizardAndEnableService()
+ }
+ }
+ )
+}
diff --git a/app/src/main/java/dev/lexip/hecate/ui/setup/PermissionSetupSteps.kt b/app/src/main/java/dev/lexip/hecate/ui/setup/PermissionSetupSteps.kt
index 92838c1..52b60e6 100644
--- a/app/src/main/java/dev/lexip/hecate/ui/setup/PermissionSetupSteps.kt
+++ b/app/src/main/java/dev/lexip/hecate/ui/setup/PermissionSetupSteps.kt
@@ -337,14 +337,13 @@ internal fun ConnectUsbStep(
OutlinedButton(onClick = onExit) {
Text(text = stringResource(id = R.string.action_close))
}
- Button(
+ OutlinedButton(
onClick = {
haptic.performHapticFeedback(HapticFeedbackType.ContextClick)
onNext()
- },
- enabled = isUsbConnected
+ }
) {
- Text(text = stringResource(id = R.string.action_continue))
+ Text(text = stringResource(id = R.string.action_skip))
}
}
}
@@ -373,6 +372,30 @@ private fun ConnectionWhySection() {
)
}
}
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ )
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = stringResource(id = R.string.permission_wizard_is_this_safe_title),
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = stringResource(id = R.string.permission_wizard_is_this_safe_body),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
}
@Composable
@@ -501,7 +524,7 @@ private fun WebsiteShareCard(
modifier = Modifier.wrapContentWidth(),
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 6.dp)
) {
- Text(text = stringResource(id = R.string.action_share_setup_url))
+ Text(text = stringResource(id = R.string.action_share_url))
}
}
}
diff --git a/app/src/main/java/dev/lexip/hecate/ui/setup/ShareExtensions.kt b/app/src/main/java/dev/lexip/hecate/ui/setup/ShareExtensions.kt
new file mode 100644
index 0000000..3e5b8e0
--- /dev/null
+++ b/app/src/main/java/dev/lexip/hecate/ui/setup/ShareExtensions.kt
@@ -0,0 +1,19 @@
+package dev.lexip.hecate.ui.setup
+
+import android.content.Intent
+
+// Helper to share a URL via Android Sharesheet, reused by setup components.
+internal fun android.content.Context.shareSetupUrl(url: String) {
+ if (url.isBlank()) return
+
+ val sendIntent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_TEXT, url)
+ putExtra(Intent.EXTRA_TITLE, "Setup - Adaptive Theme")
+ type = "text/plain"
+ }
+
+ val shareIntent = Intent.createChooser(sendIntent, null)
+ startActivity(shareIntent)
+}
+
diff --git a/app/src/main/java/dev/lexip/hecate/util/InstallSourceChecker.kt b/app/src/main/java/dev/lexip/hecate/util/InstallSourceChecker.kt
new file mode 100644
index 0000000..889e93f
--- /dev/null
+++ b/app/src/main/java/dev/lexip/hecate/util/InstallSourceChecker.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2025 xLexip
+ *
+ * Licensed under the GNU General Public License, Version 3.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.gnu.org/licenses/gpl-3.0
+ *
+ * Please see the License for specific terms regarding permissions and limitations.
+ */
+
+package dev.lexip.hecate.util
+
+import android.content.Context
+import android.os.Build
+import android.util.Log
+
+object InstallSourceChecker {
+
+ private const val TAG = "InstallSourceChecker"
+ private const val PLAY_STORE_PACKAGE = "com.android.vending"
+
+ /**
+ * Returns true only if the app was installed from the Google Play Store.
+ * On any error or unknown installer, this returns false to safely disable in-app updates.
+ */
+ fun isInstalledFromPlayStore(context: Context): Boolean {
+ val installer = getInstallerPackageName(context)
+ val fromPlay = installer == PLAY_STORE_PACKAGE
+ Log.d(TAG, "Installer package: $installer, isPlayStore=$fromPlay")
+ return fromPlay
+ }
+
+ private fun getInstallerPackageName(context: Context): String? {
+ val pm = context.packageManager
+ val packageName = context.packageName
+ return try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ pm.getInstallSourceInfo(packageName).installingPackageName
+ } else {
+ @Suppress("DEPRECATION")
+ pm.getInstallerPackageName(packageName)
+ }
+ } catch (t: Throwable) {
+ Log.w(TAG, "Failed to resolve installer package", t)
+ null
+ }
+ }
+}
+
diff --git a/app/src/main/java/dev/lexip/hecate/util/LightSensorManager.kt b/app/src/main/java/dev/lexip/hecate/util/LightSensorManager.kt
index 7b05dfa..2471250 100644
--- a/app/src/main/java/dev/lexip/hecate/util/LightSensorManager.kt
+++ b/app/src/main/java/dev/lexip/hecate/util/LightSensorManager.kt
@@ -28,10 +28,13 @@ class LightSensorManager(private val context: Context) : SensorEventListener {
private val lightSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)
private lateinit var callback: (Float) -> Unit
- fun startListening(callback: (Float) -> Unit) {
+ fun startListening(
+ callback: (Float) -> Unit,
+ sensorDelay: Int = SensorManager.SENSOR_DELAY_FASTEST
+ ) {
this.callback = callback
lightSensor?.let {
- sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_FASTEST)
+ sensorManager.registerListener(this, it, sensorDelay)
}
}
diff --git a/app/src/main/java/dev/lexip/hecate/util/ProximitySensorManager.kt b/app/src/main/java/dev/lexip/hecate/util/ProximitySensorManager.kt
index 68cf2d7..daf3b21 100644
--- a/app/src/main/java/dev/lexip/hecate/util/ProximitySensorManager.kt
+++ b/app/src/main/java/dev/lexip/hecate/util/ProximitySensorManager.kt
@@ -28,10 +28,13 @@ class ProximitySensorManager(private val context: Context) : SensorEventListener
private val proximitySensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)
private lateinit var callback: (Float) -> Unit
- fun startListening(callback: (Float) -> Unit) {
+ fun startListening(
+ callback: (Float) -> Unit,
+ sensorDelay: Int = SensorManager.SENSOR_DELAY_FASTEST
+ ) {
this.callback = callback
proximitySensor?.let {
- sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_FASTEST)
+ sensorManager.registerListener(this, it, sensorDelay)
}
}
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 91a8285..4f9a5e6 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -1,6 +1,4 @@
-
-
Abbrechen
Schließen
Weiter
@@ -9,24 +7,28 @@
Fertig
Festlegen
Teilen
- Link teilen
- Dienst stoppen
- Adaptive Theme verwenden
+ Link teilen
+ %1$s verwenden
+ Überspringen
+ Service pausieren
- Wechselt automatisch zwischen hellem und dunklem Modus basierend auf dem Umgebungslicht – für bessere Sichtbarkeit und Akkulaufzeit. Der Wechsel erfolgt nur beim Einschalten des Bildschirms und nur, wenn das Gerät nicht abgedeckt ist.
+ Wechselt automatisch zwischen Hell- und Dunkelmodus basierend auf dem Umgebungslicht. Das spart Akku und verbessert die Lesbarkeit. Das Design ändert sich nur direkt nachdem du den Bildschirm einschaltest und nur, wenn der Sensor nicht verdeckt ist.
- Systemtheme wird anhand der Umgebungshelligkeit angepasst.
+ Passt das Systemdesign an die Umgebungshelligkeit an.
- Ungültiger Lux-Wert.
- Der Lux-Wert kann maximal 100.000 sein.
- Der Lux-Wert darf nicht negativ sein.
+ Sensor verdeckt
+ Theme-Änderungen sind pausiert, da der Lichtsensor verdeckt ist.
+
Keine E-Mail-App gefunden.
+ Ungültiger Helligkeitswert.
+ Wert darf nicht negativ sein.
+ Wert darf 100.000 nicht überschreiten.
- Info
+ Über
Helligkeitsschwelle
Sprache ändern
Aktuelle Helligkeit
@@ -45,29 +47,34 @@
Lux-Wert
-
- Einstellungen öffnen
+ Entwickleroptionen öffnen
Einstellungen öffnen
- Um die Berechtigung zu erteilen, benötigen Sie ein weiteres Gerät (vorzugsweise einen Computer) mit Webbrowser. Verbinden Sie dieses Gerät per USB-Kabel damit.
Mit einem anderen Gerät verbinden
- In die Zwischenablage kopiert.
- Tippen Sie einfach mehrmals auf die Build-Nummer, um die Entwickleroptionen freizuschalten, und aktivieren Sie dort USB-Debugging.
+ Um die Berechtigung zu erteilen, benötigst du ein anderes Gerät (Computer, Tablet oder Smartphone). Verbinde dieses Gerät per USB damit.
+ Command kopiert.
Entwicklermodus aktivieren
- Entwickleroptionen aktiviert
+ Aktiviere USB-Debugging in den Entwickleroptionen.
Entwickleroptionen aktivieren
- Mehrfach auf die Build-Nummer tippen, um die Entwickleroptionen zu aktivieren.
- Alternative für Experten
- Öffne diese Website auf einem anderen Gerät und folge den Anweisungen dort:
- Berechtigung erteilen
- Falls ADB auf dem anderen Gerät installiert ist, kannst du dort stattdessen diesen Command ausführen:
- Berechtigung erfolgreich erteilt!
- Berechtigung noch nicht erteilt. Schließen Sie die Einrichtung auf dem anderen Gerät ab.
+ Entwickleroptionen aktiviert
+ Finde die Build-Nummer und tippe 7-mal darauf, um die Entwickleroptionen zu aktivieren.
+ Alternative für Experten (ADB)
+ Berechtigung mit dem anderen Gerät erteilen
+ Öffne den folgenden Link auf deinem anderen Gerät und folge den Anweisungen:
+ Wenn du ADB auf deinem Computer installiert hast, kannst du stattdessen diesen Command ausführen:
+ Berechtigung erteilt.
+ Berechtigung noch nicht erteilt. Bitte schließe die Einrichtung mit dem anderen Gerät ab.
Schritt %1$d von %2$d
USB verbunden
USB-Debugging aktivieren
USB-Debugging aktiviert
- USB-Debugging aktivieren
- Warten auf USB-Verbindung …
- Die Berechtigung kann nur über ADB erteilt werden, wofür ein anderes Gerät mit einem Webbrowser oder ADB erforderlich ist.
- Warum wird ein anderes Gerät benötigt?
-
+ Aktiviere USB-Debugging
+ Warte auf USB-Verbindung...
+ Warum ist ein anderes Gerät nötig?
+ Android verhindert, dass Apps sich die benötigte Berechtigung selbst erteilen. Es wird ein anderes Gerät mit einem Webbrowser oder ADB benötigt.
+ Ist das sicher?
+ Ja. Es erlaubt der App nur, Einstellungen wie den Dunkelmodus zu ändern. Das ist absolut sicher und vollständig rückgängig zu machen.
+
+ Einrichtung erforderlich
+ %1$s benötigt eine einmalige Einrichtung, um zu funktionieren.
+ Einrichtung starten
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 66d7063..ce28f28 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,8 +1,8 @@
Adaptive Theme
lexip.dev/setup
+ Star on GitHub
-
Cancel
Close
Continue
@@ -11,21 +11,26 @@
Finish
Set
Share
- Share Link
- Stop Service
- Use Adaptive Theme
+ Share Link
+ Skip
+ Pause Service
+ Use %1$s
- Automatically switches between Light and Dark mode based on ambient light for better visibility and battery life. Theme changes occur only when the screen turns on and the device is uncovered.
+ Automatically switches between Light and Dark mode based on ambient light. This saves battery and improves readability. The theme only changes right after you turn the screen on and only when the sensor is not covered.
+
Adjusting the system theme based on ambient brightness.
+ Sensor Covered
+ Theme changes are paused because the light sensor is covered.
+
No email app found.
- Invalid lux value.
- Lux value cannot be negative.
- Lux value cannot exceed 100,000.
+ Invalid brightness value.
+ Value cannot be negative.
+ Value cannot exceed 100,000.
About
Brightness Threshold
@@ -33,7 +38,7 @@
Current Brightness
Custom Threshold
More
- Background Activity
+ Theme Switching Service
Send Feedback
Bright
@@ -46,29 +51,34 @@
Lux value
-
- Open Settings
+ Open Developer Options
Open Settings
- To grant the permission, you\'ll need another device (preferably a computer) with a web browser. Connect this device to it using a USB cable.
- Connect to another device
- Command copied to clipboard.
- Tap the build number several times to unlock Developer options, then enable USB debugging there.
+ Connect to setup device
+ To grant the permission, you need a secondary device (computer, tablet, or phone). Connect this device to it via USB.
+ Command copied.
Enable Developer Mode
- Developer options enabled
+ Enable USB-Debugging in the developer options.
Enable developer options
- Look for the build number and tap it 7 times to enable the developer settings. You\'ll see a message confirming it\'s enabled.
- Alternative for experts
- On your other device, open this website and follow the instructions:
- Grant the permission
+ Developer options enabled
+ Find the Build number and tap it 7 times to enable developer options.
+ Alternative for experts (ADB)
+ Grant permission with the other device
+ Open the following link on your other device and follow the instructions:
If you have ADB installed on your computer, you can run this command instead:
- Permission granted successfully!
- Permission not yet granted. Complete the setup on your other device.
+ Permission granted.
+ Permission not yet granted. Please complete the setup with the other device.
Step %1$d of %2$d
USB connected
- Enable USB debugging
- USB debugging enabled
- Enable USB debugging
- Waiting for USB connection
- The permission can only be granted via ADB, which requires another device with either a web browser or ADB installed.
+ Enable USB Debugging
+ USB Debugging enabled
+ Enable USB Debugging
+ Waiting for USB connection...
Why is another device required?
-
+ Android prevents apps from granting the required permission themselves. It requires another device with either a web browser or ADB installed.
+ Is this safe?
+ Yes. It just allows the app to modify settings like the dark mode. This is absolutely safe and completely reversible.
+
+ Setup Required
+ %1$s requires a one-time setup to function.
+ Start Setup
+
\ No newline at end of file