diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3368ba747..827be7f42 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,8 +29,8 @@ android { applicationId = "com.sameerasw.essentials" minSdk = 26 targetSdk = 37 - versionCode = 50 - versionName = "15.6" + versionCode = 51 + versionName = "15.7" val whatsNewCounter = 2 buildConfigField("int", "WHATS_NEW_COUNTER", whatsNewCounter.toString()) diff --git a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt index b624484d4..5df6ff98f 100644 --- a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt @@ -304,7 +304,9 @@ class FeatureSettingsActivity : AppCompatActivity() { "Location reached" -> !viewModel.isLocationPermissionGranted.value || !viewModel.isBackgroundLocationPermissionGranted.value "Quick settings tiles" -> !viewModel.isWriteSettingsEnabled.value - "Screen refresh rate" -> !viewModel.isShizukuPermissionGranted.value + "Screen refresh rate" -> !com.sameerasw.essentials.utils.ShellUtils.hasPermission( + context + ) // Top level checks for other features (rarely hit if they are children, but safe to add) "Essentials On Display" -> !isAccessibilityEnabled || !isNotificationListenerEnabled "Call vibrations" -> !isReadPhoneStateEnabled || !isNotificationListenerEnabled @@ -539,7 +541,9 @@ class FeatureSettingsActivity : AppCompatActivity() { "Battery notification" -> !viewModel.isPostNotificationsEnabled.value || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !viewModel.isBluetoothPermissionGranted.value) "Text and animations" -> !viewModel.isWriteSettingsEnabled.value || !isWriteSecureSettingsEnabled "Lock screen clock" -> !isWriteSecureSettingsEnabled - "Screen refresh rate" -> !viewModel.isShizukuPermissionGranted.value + "Screen refresh rate" -> !com.sameerasw.essentials.utils.ShellUtils.hasPermission( + context + ) "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isUsageStatsPermissionGranted.value else -> false } diff --git a/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt index 2db18abd9..eba275919 100644 --- a/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt @@ -92,10 +92,12 @@ import com.sameerasw.essentials.ui.components.pickers.DefaultTabPicker import com.sameerasw.essentials.ui.components.pickers.LanguagePicker import com.sameerasw.essentials.ui.components.sheets.InstructionsBottomSheet import com.sameerasw.essentials.ui.components.sheets.UnsupportedFeaturesConfirmationSheet +import com.sameerasw.essentials.ui.components.sheets.ImportConfigConfirmationSheet import com.sameerasw.essentials.ui.components.sheets.UpdateBottomSheet import com.sameerasw.essentials.ui.modifiers.BlurDirection import com.sameerasw.essentials.ui.modifiers.progressiveBlur import com.sameerasw.essentials.ui.theme.EssentialsTheme +import com.sameerasw.essentials.ui.theme.Shapes import com.sameerasw.essentials.utils.DeviceUtils import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.utils.PermissionUtils @@ -261,6 +263,30 @@ fun SettingsContent( var showInstructionsSheet by remember { mutableStateOf(false) } var showShizukuHelpBottomSheet by remember { mutableStateOf(false) } var showUnsupportedFeaturesSheet by remember { mutableStateOf(false) } + var showImportConfirmSheet by remember { mutableStateOf(false) } + var selectedImportUri by remember { mutableStateOf(null) } + + val onImportConfig: (Boolean) -> Unit = { keepPrefs -> + selectedImportUri?.let { uri -> + try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + if (viewModel.importConfigs(context, inputStream, keepPrefs)) { + Toast.makeText(context, "Config imported successfully", Toast.LENGTH_SHORT) + .show() + } else { + Toast.makeText(context, "Failed to import config", Toast.LENGTH_SHORT) + .show() + } + } + } catch (e: Exception) { + Toast.makeText(context, "Failed to import config", Toast.LENGTH_SHORT).show() + e.printStackTrace() + } finally { + selectedImportUri = null + showImportConfirmSheet = false + } + } + } val exportLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.CreateDocument("application/json") @@ -283,20 +309,8 @@ fun SettingsContent( contract = ActivityResultContracts.OpenDocument() ) { uri -> uri?.let { - try { - context.contentResolver.openInputStream(it)?.use { inputStream -> - if (viewModel.importConfigs(context, inputStream)) { - Toast.makeText(context, "Config imported successfully", Toast.LENGTH_SHORT) - .show() - } else { - Toast.makeText(context, "Failed to import config", Toast.LENGTH_SHORT) - .show() - } - } - } catch (e: Exception) { - Toast.makeText(context, "Failed to import config", Toast.LENGTH_SHORT).show() - e.printStackTrace() - } + selectedImportUri = it + showImportConfirmSheet = true } } @@ -325,6 +339,21 @@ fun SettingsContent( ) } + if (showImportConfirmSheet) { + ImportConfigConfirmationSheet( + onDismissRequest = { + showImportConfirmSheet = false + selectedImportUri = null + }, + onConfirmOverride = { + onImportConfig(false) + }, + onConfirmMerge = { + onImportConfig(true) + } + ) + } + if (showShizukuHelpBottomSheet) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ModalBottomSheet( @@ -956,7 +985,8 @@ fun SettingsContent( modifier = Modifier .fillMaxWidth() .background( - color = MaterialTheme.colorScheme.surfaceBright + color = MaterialTheme.colorScheme.surfaceBright, + shape = Shapes.extraSmall ) .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) @@ -987,7 +1017,8 @@ fun SettingsContent( modifier = Modifier .fillMaxWidth() .background( - color = MaterialTheme.colorScheme.surfaceBright + color = MaterialTheme.colorScheme.surfaceBright, + shape = Shapes.extraSmall ) .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) @@ -1026,7 +1057,8 @@ fun SettingsContent( modifier = Modifier .fillMaxWidth() .background( - color = MaterialTheme.colorScheme.surfaceBright + color = MaterialTheme.colorScheme.surfaceBright, + shape = Shapes.extraSmall ) .padding(start = 12.dp, end = 12.dp, top = 4.dp, bottom = 12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt index 255d2446a..de997eca6 100644 --- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt @@ -52,6 +52,8 @@ class SettingsRepository(private val context: Context) { const val KEY_DAILY_WALLPAPER_PHOTO_LINK = "daily_wallpaper_photo_link" const val KEY_DAILY_WALLPAPER_UPDATED_AT = "daily_wallpaper_updated_at" const val KEY_DAILY_WALLPAPER_AUTO_UPDATE = "daily_wallpaper_auto_update" + const val KEY_DAILY_WALLPAPER_AUTO_UPDATE_TIME = "daily_wallpaper_auto_update_time" + const val KEY_DAILY_WALLPAPER_SHOW_LAST_TIME = "daily_wallpaper_show_last_time" const val KEY_DAILY_WALLPAPER_APPLY_HOME = "daily_wallpaper_apply_home" const val KEY_DAILY_WALLPAPER_APPLY_LOCK = "daily_wallpaper_apply_lock" @@ -121,6 +123,7 @@ class SettingsRepository(private val context: Context) { const val KEY_FLASHLIGHT_PULSE_MAX_INTENSITY = "flashlight_pulse_max_intensity" const val KEY_FLASHLIGHT_PULSE_DISABLE_ON_DND = "flashlight_pulse_disable_on_dnd" const val KEY_FLASHLIGHT_POCKET_TURN_OFF_ENABLED = "flashlight_pocket_turn_off_enabled" + const val KEY_FLASHLIGHT_OVERHEAT_PREVENTION_ENABLED = "flashlight_overheat_prevention_enabled" const val KEY_SCREEN_LOCKED_SECURITY_ENABLED = "screen_locked_security_enabled" const val KEY_HIDE_SYSTEM_ICONS = "hide_system_icons" @@ -286,6 +289,7 @@ class SettingsRepository(private val context: Context) { const val KEY_POCKET_MODE_EXCLUDED_APPS = "pocket_mode_excluded_apps" const val KEY_POCKET_MODE_TRIGGER_DELAY = "pocket_mode_trigger_delay" const val KEY_POCKET_MODE_LOCK_SCREEN_ONLY = "pocket_mode_lock_screen_only" + const val KEY_KEEP_PREFS = "keep_prefs" } // Observe changes @@ -767,7 +771,7 @@ class SettingsRepository(private val context: Context) { } } - fun importConfigs(inputStream: java.io.InputStream): Boolean { + fun importConfigs(inputStream: java.io.InputStream, keepPrefs: Boolean): Boolean { return try { val json = inputStream.bufferedReader().use { it.readText() } val allConfigs: Map>> = @@ -795,7 +799,7 @@ class SettingsRepository(private val context: Context) { } p.edit().apply { - clear() + if(!keepPrefs) clear() // Restore preserved values preservedValues.forEach { (key, value) -> diff --git a/app/src/main/java/com/sameerasw/essentials/services/DailyWallpaperWorker.kt b/app/src/main/java/com/sameerasw/essentials/services/DailyWallpaperWorker.kt index eff3528b7..3f5ac267b 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/DailyWallpaperWorker.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/DailyWallpaperWorker.kt @@ -6,6 +6,7 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.sameerasw.essentials.data.repository.SettingsRepository import com.sameerasw.essentials.data.repository.WallpaperRepository +import java.time.LocalDateTime class DailyWallpaperWorker( appContext: Context, @@ -77,6 +78,13 @@ class DailyWallpaperWorker( SettingsRepository.KEY_DAILY_WALLPAPER_UPDATED_AT, todayWallpaper.updatedAt ) + + val currentTime = LocalDateTime.now().toString() + settingsRepository.putString( + SettingsRepository.KEY_DAILY_WALLPAPER_AUTO_UPDATE_TIME, + currentTime + ) + Log.d( "DailyWallpaperWorker", "Successfully auto-applied wallpaper (force=$force, flags=$flags): ${todayWallpaper.id}" diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt index 80f4fdc69..8fe150c45 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt @@ -51,6 +51,7 @@ class FlashlightHandler( private var primaryCameraId: String? = null private var currentIntensityLevel: Int = 1 private var flashlightJob: Job? = null + private var overheatPreventionJob: Job? = null private var isInternalToggle = false private val NOTIFICATION_ID_FLASHLIGHT = 1010 @@ -105,11 +106,13 @@ class FlashlightHandler( currentIntensityLevel = FlashlightUtil.getDefaultLevel(service, cameraId) updateFlashlightNotification(currentIntensityLevel) } + startOverheatPrevention(cameraId) } else { // Flashlight turned OFF flashlightJob?.cancel() // Stop any ongoing fade-in or fade-out isInternalToggle = false // Reset cancelFlashlightNotification() + stopOverheatPrevention() } } } @@ -121,6 +124,7 @@ class FlashlightHandler( fun unregister() { torchCallback.let { cameraManager.unregisterTorchCallback(it) } primaryCameraId = null + stopOverheatPrevention() } fun handleIntent(intent: Intent) { @@ -657,4 +661,37 @@ class FlashlightHandler( false } } + + private fun startOverheatPrevention(cameraId: String) { + overheatPreventionJob?.cancel() + overheatPreventionJob = scope.launch { + while (isTorchOn) { + kotlinx.coroutines.delay(120000L) + val prefs = service.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) + val isOverheatEnabled = prefs.getBoolean("flashlight_overheat_prevention_enabled", true) + if (isOverheatEnabled && FlashlightUtil.isIntensitySupported(service, cameraId)) { + val currentLevel = FlashlightUtil.getCurrentLevel(service, cameraId) + val maxLevel = FlashlightUtil.getMaxLevel(service, cameraId) + val limitLevel = (maxLevel * 0.75f).toInt().coerceAtLeast(1) + if (currentLevel > limitLevel) { + FlashlightUtil.fadeFlashlight( + service, + cameraId, + fromLevel = currentLevel, + toLevel = limitLevel, + durationMs = 1000L, + steps = 15 + ) + currentIntensityLevel = limitLevel + updateFlashlightNotification(limitLevel) + } + } + } + } + } + + private fun stopOverheatPrevention() { + overheatPreventionJob?.cancel() + overheatPreventionJob = null + } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/YourAndroidActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/YourAndroidActivity.kt index 0de9c864f..6bf4a8e8e 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/YourAndroidActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/YourAndroidActivity.kt @@ -16,15 +16,15 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll @@ -74,6 +74,7 @@ import com.sameerasw.essentials.ui.components.sheets.UpdateBottomSheet import com.sameerasw.essentials.ui.modifiers.BlurDirection import com.sameerasw.essentials.ui.modifiers.progressiveBlur import com.sameerasw.essentials.ui.theme.EssentialsTheme +import com.sameerasw.essentials.ui.theme.Shapes import com.sameerasw.essentials.utils.DeviceInfo import com.sameerasw.essentials.utils.DeviceUtils import com.sameerasw.essentials.utils.HapticUtil @@ -527,20 +528,20 @@ fun YourAndroidContent( .padding(32.dp) ) } else if (trackedRepos.isEmpty()) { - androidx.compose.material3.Card( - modifier = Modifier - .background( - MaterialTheme.colorScheme.surfaceBright - ) - .fillMaxWidth() - .graphicsLayer { - alpha = contentAlphaState.value - translationY = contentOffsetState.value.toPx() - }, - shape = MaterialTheme.shapes.large + RoundedCardContainer( + modifier = Modifier.graphicsLayer { + alpha = contentAlphaState.value + translationY = contentOffsetState.value.toPx() + } ) { Column( - modifier = Modifier.padding(24.dp), + modifier = Modifier + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.surfaceBright, + shape = Shapes.extraSmall + ) + .padding(32.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { @@ -549,17 +550,29 @@ fun YourAndroidContent( style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = onAddRepoClick) { - Text(stringResource(R.string.action_add_repository)) - } - Spacer(modifier = Modifier.height(24.dp)) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.surfaceBright, + shape = Shapes.extraSmall + ) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { ImportExportButtons( + modifier = Modifier.wrapContentWidth().weight(1f), view = view, exportLauncher = exportLauncher, importLauncher = importLauncher, showExport = false ) + Button(onClick = onAddRepoClick, modifier = Modifier.weight(1f)) { + Text(stringResource(R.string.action_add_repository)) + } } } } else { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ImportConfigConfirmationSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ImportConfigConfirmationSheet.kt new file mode 100644 index 000000000..526e86477 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ImportConfigConfirmationSheet.kt @@ -0,0 +1,119 @@ +package com.sameerasw.essentials.ui.components.sheets + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.utils.HapticUtil + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ImportConfigConfirmationSheet( + onDismissRequest: () -> Unit, + onConfirmOverride: () -> Unit, + onConfirmMerge: () -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val view = LocalView.current + + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.import_config_sheet_title), + style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + + Text( + text = stringResource(R.string.import_config_sheet_desc), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + RoundedCardContainer( + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.import_config_merge_warning), + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium), + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center + ) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = { + HapticUtil.performUIHaptic(view) + onConfirmOverride() + }, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(R.string.action_override)) + } + + OutlinedButton( + onClick = { + HapticUtil.performUIHaptic(view) + onConfirmMerge() + }, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(R.string.action_merge)) + } + } + + OutlinedButton( + onClick = { + HapticUtil.performUIHaptic(view) + onDismissRequest() + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.action_abort)) + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FlashlightPulseSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FlashlightPulseSettingsUI.kt index a9841d491..6163cb206 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FlashlightPulseSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FlashlightPulseSettingsUI.kt @@ -85,9 +85,9 @@ fun FlashlightPulseSettingsUI( title = stringResource(R.string.flashlight_pulse_max_brightness), value = viewModel.flashlightPulseMaxIntensity.floatValue, onValueChange = { viewModel.setFlashlightPulseMaxIntensity(it) }, - valueRange = 0.05f..1f, - valueFormatter = { "${(it * 100).toInt()}%" }, - increment = 0.05f, + valueRange = 0.01f..1f, + valueFormatter = { "${Math.round(it * 100)}%" }, + increment = 0.01f, modifier = Modifier.highlight(highlightSetting == "flashlight_pulse_max_intensity") ) } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FlashlightSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FlashlightSettingsUI.kt index 3ccea7595..cb707df60 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FlashlightSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FlashlightSettingsUI.kt @@ -71,6 +71,13 @@ fun FlashlightSettingsUI( isChecked = viewModel.isFlashlightPocketTurnOffEnabled.value, onCheckedChange = { viewModel.setFlashlightPocketTurnOffEnabled(it, context) } ) + IconToggleItem( + iconRes = R.drawable.rounded_thermometer_alert_24, + title = stringResource(R.string.flashlight_overheat_title), + description = stringResource(R.string.flashlight_overheat_desc), + isChecked = viewModel.isFlashlightOverheatEnabled.value, + onCheckedChange = { viewModel.setFlashlightOverheatEnabled(it, context) } + ) } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/RefreshRateSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/RefreshRateSettingsUI.kt index 2f4e73ef2..35a79e30a 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/RefreshRateSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/RefreshRateSettingsUI.kt @@ -38,7 +38,7 @@ fun RefreshRateSettingsUI( ) { val context = LocalContext.current val view = LocalView.current - val isEnabled = viewModel.isShizukuPermissionGranted.value + val isEnabled = if (viewModel.isRootEnabled.value) viewModel.isRootPermissionGranted.value else viewModel.isShizukuPermissionGranted.value val isFixedMode = viewModel.refreshRateMode.value == RefreshRateUtils.MODE_FIXED val systemLabel = stringResource(R.string.refresh_rate_system_default) @@ -172,7 +172,10 @@ fun RefreshRateSettingsUI( } } else { Text( - text = stringResource(R.string.msg_refresh_rate_permission_required), + text = stringResource( + if (viewModel.isRootEnabled.value) R.string.msg_refresh_rate_root_permission_required + else R.string.msg_refresh_rate_permission_required + ), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.error, modifier = Modifier @@ -181,7 +184,11 @@ fun RefreshRateSettingsUI( ) Button( onClick = { - viewModel.requestShizukuPermission() + if (viewModel.isRootEnabled.value) { + viewModel.check(context) + } else { + viewModel.requestShizukuPermission() + } HapticUtil.performSliderHaptic(view) }, colors = ButtonDefaults.filledTonalButtonColors(), diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/wallpaper/WallpaperScreen.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/wallpaper/WallpaperScreen.kt index 6633e91cd..19ba61264 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/wallpaper/WallpaperScreen.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/wallpaper/WallpaperScreen.kt @@ -70,6 +70,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -107,6 +108,9 @@ import com.sameerasw.essentials.viewmodels.MainViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.time.Duration +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -124,6 +128,8 @@ fun WallpaperScreen( val isLoading by viewModel.isWallpaperLoading val isBlurEnabled by viewModel.isBlurEnabled val isAutoUpdateEnabled by viewModel.isDailyWallpaperAutoUpdateEnabled + val dailyWallpaperAutoUpdateTime by viewModel.dailyWallpaperAutoUpdateTime + val isDailyWallpaperShowLastTime by viewModel.isDailyWallpaperShowLastTime // Live Wallpaper Settings State var availableVideos by remember { mutableStateOf(repository.getLiveWallpaperAvailableVideos()) } @@ -212,6 +218,24 @@ fun WallpaperScreen( } } + var dailyWallpaperNextAutoUpdateTime by remember { mutableLongStateOf(0L) } + var dailyWallpaperLastCheckedFormatted by remember { mutableStateOf("") } + LaunchedEffect(dailyWallpaperAutoUpdateTime) { + if (!dailyWallpaperAutoUpdateTime.isNullOrEmpty()){ + val prevTime = runCatching { LocalDateTime.parse(dailyWallpaperAutoUpdateTime) }.getOrNull() + if (prevTime != null) { + val passedTime = Duration.between( + prevTime, + LocalDateTime.now() + ).toHours() + dailyWallpaperNextAutoUpdateTime = (12L - passedTime).coerceAtLeast(0) + dailyWallpaperLastCheckedFormatted = runCatching { + prevTime.format(DateTimeFormatter.ofPattern("hh:mm a")) + }.getOrDefault("") + } + } + } + Box( modifier = Modifier .fillMaxSize() @@ -473,24 +497,49 @@ fun WallpaperScreen( .padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = stringResource(R.string.label_wallpaper_auto_update), - style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), - color = MaterialTheme.colorScheme.onSurface - ) - Switch( - checked = isAutoUpdateEnabled, - onCheckedChange = { checked -> - HapticUtil.performUIHaptic(view) - viewModel.setDailyWallpaperAutoUpdate(checked, context) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = stringResource(R.string.label_wallpaper_auto_update), + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onSurface + ) + if (isAutoUpdateEnabled && !dailyWallpaperAutoUpdateTime.isNullOrEmpty()){ + val timeText = if(isDailyWallpaperShowLastTime) { + stringResource(R.string.label_wallpaper_last_checked, dailyWallpaperLastCheckedFormatted) + } else{ + val showTime = if(dailyWallpaperNextAutoUpdateTime > 0L) { + stringResource(R.string.label_wallpaper_hours, dailyWallpaperNextAutoUpdateTime.toInt()) + } else { + stringResource(R.string.label_wallpaper_a_few_minutes) + } + stringResource(R.string.label_wallpaper_next_check, showTime) + } + Text( + text = timeText, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.clickable(enabled = true, onClick = { + HapticUtil.performUIHaptic(view) + viewModel.setDailyWallpaperShowLastTime() + }) + ) + } } - ) - } + Switch( + checked = isAutoUpdateEnabled, + onCheckedChange = { checked -> + HapticUtil.performUIHaptic(view) + viewModel.setDailyWallpaperAutoUpdate(checked, context) + } + ) + } Row( modifier = Modifier diff --git a/app/src/main/java/com/sameerasw/essentials/utils/PermissionUIHelper.kt b/app/src/main/java/com/sameerasw/essentials/utils/PermissionUIHelper.kt index abc65df74..442b45793 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/PermissionUIHelper.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/PermissionUIHelper.kt @@ -55,11 +55,17 @@ object PermissionUIHelper { viewModel.check(context) }, isGranted = viewModel.isWriteSecureSettingsEnabled.value, - shizukuActionLabel = R.string.perm_action_grant_shizuku, - shizukuActionEnabled = ShizukuUtils.hasPermission(), + shizukuActionLabel = if (viewModel.isRootEnabled.value) R.string.perm_action_grant_root else R.string.perm_action_grant_shizuku, + shizukuActionEnabled = if (viewModel.isRootEnabled.value) viewModel.isRootAvailable.value else ShizukuUtils.hasPermission(), shizukuAction = { - if (ShizukuUtils.grantWriteSecureSettingsPermission()) { - viewModel.check(context) + if (viewModel.isRootEnabled.value) { + if (RootUtils.runCommand("pm grant ${context.packageName} android.permission.WRITE_SECURE_SETTINGS")) { + viewModel.check(context) + } + } else { + if (ShizukuUtils.grantWriteSecureSettingsPermission()) { + viewModel.check(context) + } } }, instructions = R.string.perm_write_secure_instructions @@ -203,13 +209,23 @@ object PermissionUIHelper { ) "SHIZUKU" -> PermissionItem( - iconRes = R.drawable.rounded_mode_cool_24, - title = R.string.perm_shizuku_title, - description = R.string.perm_shizuku_desc, + iconRes = if (viewModel.isRootEnabled.value) R.drawable.rounded_numbers_24 else R.drawable.rounded_mode_cool_24, + title = if (viewModel.isRootEnabled.value) R.string.perm_root_title else R.string.perm_shizuku_title, + description = if (viewModel.isRootEnabled.value) R.string.perm_root_desc else R.string.perm_shizuku_desc, dependentFeatures = PermissionRegistry.getFeatures("SHIZUKU"), - actionLabel = if (viewModel.isShizukuPermissionGranted.value) R.string.perm_action_granted else R.string.perm_action_grant, - action = { viewModel.requestShizukuPermission() }, - isGranted = viewModel.isShizukuPermissionGranted.value + actionLabel = if (viewModel.isRootEnabled.value) { + if (viewModel.isRootPermissionGranted.value) R.string.perm_action_granted else R.string.perm_action_grant + } else { + if (viewModel.isShizukuPermissionGranted.value) R.string.perm_action_granted else R.string.perm_action_grant + }, + action = { + if (viewModel.isRootEnabled.value) { + viewModel.check(context) + } else { + viewModel.requestShizukuPermission() + } + }, + isGranted = if (viewModel.isRootEnabled.value) viewModel.isRootPermissionGranted.value else viewModel.isShizukuPermissionGranted.value ) "READ_CALENDAR" -> PermissionItem( diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt index 30de864cb..f47245904 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -69,6 +69,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File import java.io.FileOutputStream +import java.time.LocalDateTime class MainViewModel : ViewModel() { val isAccessibilityEnabled = mutableStateOf(false) @@ -118,6 +119,7 @@ class MainViewModel : ViewModel() { val flashlightPulseMaxIntensity = mutableFloatStateOf(0.5f) val isFlashlightPulseDisableOnDnd = mutableStateOf(true) val isFlashlightPocketTurnOffEnabled = mutableStateOf(false) + val isFlashlightOverheatEnabled = mutableStateOf(true) val isLocationPermissionGranted = mutableStateOf(false) val isBackgroundLocationPermissionGranted = mutableStateOf(false) val isFullScreenIntentPermissionGranted = mutableStateOf(false) @@ -331,6 +333,7 @@ class MainViewModel : ViewModel() { private var workflowPollingJob: kotlinx.coroutines.Job? = null val gitHubUser = mutableStateOf(null) + private val contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { override fun onChange(selfChange: Boolean, uri: Uri?) { uri?.let { @@ -652,6 +655,10 @@ class MainViewModel : ViewModel() { isFlashlightPocketTurnOffEnabled.value = settingsRepository.getBoolean(key) } + SettingsRepository.KEY_FLASHLIGHT_OVERHEAT_PREVENTION_ENABLED -> { + isFlashlightOverheatEnabled.value = settingsRepository.getBoolean(key, true) + } + SettingsRepository.KEY_CIRCLE_TO_SEARCH_GESTURE_ENABLED -> { isCircleToSearchGestureEnabled.value = settingsRepository.getBoolean(key) } @@ -937,6 +944,8 @@ class MainViewModel : ViewModel() { if (isDailyWallpaperAutoUpdateEnabled.value) { schedulePeriodicWallpaperCheck(context) } + dailyWallpaperAutoUpdateTime.value = settingsRepository.getString(SettingsRepository.KEY_DAILY_WALLPAPER_AUTO_UPDATE_TIME) + isDailyWallpaperShowLastTime.value = settingsRepository.getBoolean(SettingsRepository.KEY_DAILY_WALLPAPER_SHOW_LAST_TIME) if (isHideGestureBarEnabled.value) { applyHideGestureBar(context, true) @@ -1312,6 +1321,8 @@ class MainViewModel : ViewModel() { ) isFlashlightPocketTurnOffEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_FLASHLIGHT_POCKET_TURN_OFF_ENABLED) + isFlashlightOverheatEnabled.value = + settingsRepository.getBoolean(SettingsRepository.KEY_FLASHLIGHT_OVERHEAT_PREVENTION_ENABLED, true) isPitchBlackThemeEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_PITCH_BLACK_THEME_ENABLED) isEnableUnsupportedFeatures.value = settingsRepository.isEnableUnsupportedFeatures() @@ -1495,6 +1506,7 @@ class MainViewModel : ViewModel() { if (isBatteryNotificationEnabled.value) { startBatteryNotificationService(context) } + } private fun startBatteryNotificationService(context: Context) { @@ -3834,6 +3846,14 @@ class MainViewModel : ViewModel() { ) } + fun setFlashlightOverheatEnabled(enabled: Boolean, context: Context) { + isFlashlightOverheatEnabled.value = enabled + settingsRepository.putBoolean( + SettingsRepository.KEY_FLASHLIGHT_OVERHEAT_PREVENTION_ENABLED, + enabled + ) + } + fun setFlashlightFadeEnabled(enabled: Boolean, context: Context) { isFlashlightFadeEnabled.value = enabled settingsRepository.putBoolean(SettingsRepository.KEY_FLASHLIGHT_FADE_ENABLED, enabled) @@ -3931,8 +3951,8 @@ class MainViewModel : ViewModel() { settingsRepository.exportConfigs(outputStream) } - fun importConfigs(context: Context, inputStream: java.io.InputStream): Boolean { - val success = settingsRepository.importConfigs(inputStream) + fun importConfigs(context: Context, inputStream: java.io.InputStream, keepPrefs: Boolean): Boolean { + val success = settingsRepository.importConfigs(inputStream, keepPrefs) if (success) { settingsRepository.syncSystemSettingsWithSaved() com.sameerasw.essentials.domain.diy.DIYRepository.reloadAutomations() @@ -4239,10 +4259,19 @@ class MainViewModel : ViewModel() { } val isDailyWallpaperAutoUpdateEnabled = mutableStateOf(false) + val dailyWallpaperAutoUpdateTime = mutableStateOf(null) + val isDailyWallpaperShowLastTime = mutableStateOf(false) + + fun setDailyWallpaperShowLastTime() { + val status = !isDailyWallpaperShowLastTime.value + isDailyWallpaperShowLastTime.value = status + settingsRepository.putBoolean(SettingsRepository.KEY_DAILY_WALLPAPER_SHOW_LAST_TIME, status) + } fun setDailyWallpaperAutoUpdate(enabled: Boolean, context: Context) { isDailyWallpaperAutoUpdateEnabled.value = enabled settingsRepository.putBoolean(SettingsRepository.KEY_DAILY_WALLPAPER_AUTO_UPDATE, enabled) + updateDailyWallpaperAutoUpdateTime(enabled) if (enabled) { schedulePeriodicWallpaperCheck(context) triggerInstantWallpaperUpdate(context) @@ -4279,4 +4308,16 @@ class MainViewModel : ViewModel() { androidx.work.WorkManager.getInstance(context) .cancelUniqueWork("daily_wallpaper_check_work") } + + private fun updateDailyWallpaperAutoUpdateTime(enabled: Boolean) { + if (enabled){ + val currentTime = LocalDateTime.now().toString() + dailyWallpaperAutoUpdateTime.value = currentTime + settingsRepository.putString(SettingsRepository.KEY_DAILY_WALLPAPER_AUTO_UPDATE_TIME, currentTime) + } else { + dailyWallpaperAutoUpdateTime.value = null + settingsRepository.remove(SettingsRepository.KEY_DAILY_WALLPAPER_AUTO_UPDATE_TIME) + } + } + } diff --git a/app/src/main/res/drawable/rounded_thermometer_alert_24.xml b/app/src/main/res/drawable/rounded_thermometer_alert_24.xml new file mode 100644 index 000000000..e4783a634 --- /dev/null +++ b/app/src/main/res/drawable/rounded_thermometer_alert_24.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e5fb0df6..67f6aa73a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -104,6 +104,8 @@ Even while display is on Pocket detection Turn off flashlight when phone is in your pocket + Overheating prevention + Lower brightness on longer period of use Settings @@ -671,6 +673,7 @@ How to grant Granted Grant using Shizuku + Grant using Root This permission can be granted either via a PC using ADB. Copy the ADB command and run on the shell.\n\nOr you can easily grant it using Shizuku from the below button.\n\nThis permission only needs to be granted once. Battery Optimization Ensure the service is not killed by the system to save power. @@ -1484,6 +1487,7 @@ System Remove the custom override and return to the system default refresh behavior Shizuku permission required to adjust refresh rate + Root permission required to adjust refresh rate Refresh Rate Mode Switch between fixed and ranged refresh rate control Fixed Refresh Rate @@ -1665,4 +1669,15 @@ Trigger delay Lock screen only Only trigger pocket mode when the device is locked + + + Restore Configuration + Choose how you want to restore the configuration. + Merging might cause data corruption. Please proceed with caution! + Override + Merge + Last checked at %1$s + Next check in %1$s + %1$d hours + a few minutes