diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 01c0bf55a..01d89927b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -215,6 +215,14 @@ android:taskAffinity="" android:theme="@style/Theme.Essentials.Translucent" /> + + + + + + + + + + + + uri?.let { + try { + context.contentResolver.openOutputStream(it)?.use { outputStream -> + updatesViewModel.exportTrackedRepos(context, outputStream) + Toast.makeText( + context, + context.getString(R.string.msg_export_success), + Toast.LENGTH_SHORT + ).show() + } + } catch (e: Exception) { + Toast.makeText( + context, + context.getString(R.string.msg_export_failed), + Toast.LENGTH_SHORT + ).show() + e.printStackTrace() + } + } + } + + val importLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument() + ) { uri -> + uri?.let { + try { + context.contentResolver.openInputStream(it)?.use { inputStream -> + if (updatesViewModel.importTrackedRepos(context, inputStream)) { + Toast.makeText( + context, + context.getString(R.string.msg_import_success), + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + context, + context.getString(R.string.msg_import_failed), + Toast.LENGTH_SHORT + ).show() + } + } + } catch (e: Exception) { + Toast.makeText( + context, + context.getString(R.string.msg_import_failed), + Toast.LENGTH_SHORT + ).show() + e.printStackTrace() + } + } + } + LaunchedEffect(errorMessage) { if (errorMessage != null && !showAddRepoSheet) { Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show() @@ -229,24 +297,33 @@ class AppUpdatesActivity : FragmentActivity() { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - androidx.compose.material3.Icon( - painter = painterResource(id = R.drawable.rounded_apps_24), - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) - ) androidx.compose.material3.Text( text = stringResource(R.string.msg_no_repos_tracked), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) + + Spacer(modifier = Modifier.height(32.dp)) + + androidx.compose.material3.Text( + text = stringResource(R.string.label_apps), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(bottom = 12.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + ImportExportButtons( + view = view, + exportLauncher = exportLauncher, + importLauncher = importLauncher + ) } } else { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = androidx.compose.foundation.layout.PaddingValues( top = innerPadding.calculateTopPadding() + 16.dp, - bottom = innerPadding.calculateBottomPadding() + 80.dp, + bottom = innerPadding.calculateBottomPadding() + 100.dp, start = 16.dp, end = 16.dp ), @@ -407,10 +484,84 @@ class AppUpdatesActivity : FragmentActivity() { } } } + + // Apps Section + item { + androidx.compose.material3.Text( + text = stringResource(R.string.label_apps), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp, bottom = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(8.dp)) + ImportExportButtons( + view = view, + exportLauncher = exportLauncher, + importLauncher = importLauncher + ) + } } } } } } } +} + +@Composable +private fun ImportExportButtons( + view: android.view.View, + exportLauncher: androidx.activity.result.ActivityResultLauncher, + importLauncher: androidx.activity.result.ActivityResultLauncher> +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Button( + onClick = { + HapticUtil.performUIHaptic(view) + val timeStamp = SimpleDateFormat( + "yyyyMMdd_HHmmss", + Locale.getDefault() + ).format(Date()) + exportLauncher.launch("essentials_updates_$timeStamp.json") + }, + modifier = Modifier.weight(1f), + colors = androidx.compose.material3.ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + androidx.compose.material3.Icon( + painter = painterResource(id = R.drawable.rounded_arrow_warm_up_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.action_export)) + } + + Button( + onClick = { + HapticUtil.performUIHaptic(view) + importLauncher.launch(arrayOf("application/json")) + }, + modifier = Modifier.weight(1f), + colors = androidx.compose.material3.ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + androidx.compose.material3.Icon( + painter = painterResource(id = R.drawable.rounded_arrow_cool_down_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.action_import)) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt b/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt index 5671fbead..f5d9ef151 100644 --- a/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt +++ b/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt @@ -19,6 +19,13 @@ class EssentialsApp : Application() { override fun onCreate() { super.onCreate() context = applicationContext + + try { + resources?.configuration + } catch (e: Exception) { + + } + ShizukuUtils.initialize() com.sameerasw.essentials.utils.LogManager.init(this) diff --git a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt index 61100af9e..dd5b2bffa 100644 --- a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt @@ -50,6 +50,7 @@ import com.sameerasw.essentials.ui.components.sheets.PermissionsBottomSheet import com.sameerasw.essentials.ui.composables.configs.AmbientMusicGlanceSettingsUI import com.sameerasw.essentials.ui.composables.configs.AppLockSettingsUI import com.sameerasw.essentials.ui.composables.configs.BatteriesSettingsUI +import com.sameerasw.essentials.ui.composables.configs.BatteryNotificationSettingsUI import com.sameerasw.essentials.ui.composables.configs.ButtonRemapSettingsUI import com.sameerasw.essentials.ui.composables.configs.CaffeinateSettingsUI import com.sameerasw.essentials.ui.composables.configs.DynamicNightLightSettingsUI @@ -63,6 +64,7 @@ import com.sameerasw.essentials.ui.composables.configs.ScreenOffWidgetSettingsUI import com.sameerasw.essentials.ui.composables.configs.SnoozeNotificationsSettingsUI import com.sameerasw.essentials.ui.composables.configs.SoundModeTileSettingsUI import com.sameerasw.essentials.ui.composables.configs.StatusBarIconSettingsUI +import com.sameerasw.essentials.ui.composables.configs.TextAnimationsSettingsUI import com.sameerasw.essentials.ui.composables.configs.WatchSettingsUI import com.sameerasw.essentials.ui.theme.EssentialsTheme import com.sameerasw.essentials.utils.BiometricSecurityHelper @@ -241,6 +243,8 @@ class FeatureSettingsActivity : FragmentActivity() { ) "Caffeinate" -> !viewModel.isPostNotificationsEnabled.value + "Battery notification" -> !viewModel.isPostNotificationsEnabled.value || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !viewModel.isBluetoothPermissionGranted.value) + "Text and animations" -> !viewModel.isWriteSettingsEnabled.value || !isWriteSecureSettingsEnabled else -> false } if (hasMissingPermissions) { @@ -429,6 +433,8 @@ class FeatureSettingsActivity : FragmentActivity() { ) "Caffeinate" -> !viewModel.isPostNotificationsEnabled.value + "Battery notification" -> !viewModel.isPostNotificationsEnabled.value || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !viewModel.isBluetoothPermissionGranted.value) + "Text and animations" -> !viewModel.isWriteSettingsEnabled.value || !isWriteSecureSettingsEnabled else -> false } @@ -603,6 +609,14 @@ class FeatureSettingsActivity : FragmentActivity() { ) } + "Battery notification" -> { + BatteryNotificationSettingsUI( + viewModel = viewModel, + modifier = Modifier.padding(top = 16.dp), + highlightKey = highlightSetting + ) + } + "Ambient music glance" -> { AmbientMusicGlanceSettingsUI( viewModel = viewModel, @@ -634,6 +648,14 @@ class FeatureSettingsActivity : FragmentActivity() { highlightSetting = highlightSetting ) } + + "Text and animations" -> { + TextAnimationsSettingsUI( + viewModel = viewModel, + modifier = Modifier.padding(top = 16.dp), + highlightSetting = highlightSetting + ) + } } } } diff --git a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt index 25333d141..66486d514 100644 --- a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt @@ -13,8 +13,11 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -22,6 +25,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Button import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -38,6 +42,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -57,6 +62,12 @@ import androidx.core.animation.doOnEnd import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import androidx.fragment.app.FragmentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import android.widget.Toast +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale import com.sameerasw.essentials.domain.DIYTabs import com.sameerasw.essentials.domain.registry.initPermissionRegistry import com.sameerasw.essentials.ui.components.DIYFloatingToolbar @@ -286,6 +297,44 @@ class MainActivity : FragmentActivity() { var repoToShowReleaseNotesFullName by remember { mutableStateOf(null) } val trackedRepos by updatesViewModel.trackedRepos + val exportLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument("application/json") + ) { uri -> + uri?.let { + contentResolver.openOutputStream(it)?.use { outputStream -> + updatesViewModel.exportTrackedRepos(context, outputStream) + Toast.makeText( + context, + getString(R.string.msg_export_success), + Toast.LENGTH_SHORT + ).show() + } + } + } + + val importLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument() + ) { uri -> + uri?.let { + contentResolver.openInputStream(it)?.use { inputStream -> + if (updatesViewModel.importTrackedRepos(context, inputStream)) { + updatesViewModel.loadTrackedRepos(context) + Toast.makeText( + context, + getString(R.string.msg_import_success), + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + context, + getString(R.string.msg_import_failed), + Toast.LENGTH_SHORT + ).show() + } + } + } + } + if (showAddRepoSheet) { AddRepoBottomSheet( viewModel = updatesViewModel, @@ -473,19 +522,26 @@ class MainActivity : FragmentActivity() { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - Icon( - painter = painterResource(id = R.drawable.rounded_apps_24), - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy( - alpha = 0.5f - ) - ) Text( text = stringResource(R.string.msg_no_repos_tracked), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) + + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = stringResource(R.string.label_apps), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(bottom = 12.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + ImportExportButtons( + view = view, + exportLauncher = exportLauncher, + importLauncher = importLauncher + ) } } else { val pending = @@ -689,6 +745,25 @@ class MainActivity : FragmentActivity() { } } } + + // Apps Section + item { + Text( + text = stringResource(R.string.label_apps), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding( + start = 16.dp, + bottom = 8.dp + ), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(8.dp)) + ImportExportButtons( + view = view, + exportLauncher = exportLauncher, + importLauncher = importLauncher + ) + } } } @@ -752,3 +827,61 @@ class MainActivity : FragmentActivity() { } } } + +@Composable +private fun ImportExportButtons( + view: android.view.View, + exportLauncher: androidx.activity.result.ActivityResultLauncher, + importLauncher: androidx.activity.result.ActivityResultLauncher> +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Button( + onClick = { + HapticUtil.performUIHaptic(view) + val timeStamp = SimpleDateFormat( + "yyyyMMdd_HHmmss", + Locale.getDefault() + ).format(Date()) + exportLauncher.launch("essentials_updates_$timeStamp.json") + }, + modifier = Modifier.weight(1f), + colors = androidx.compose.material3.ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_arrow_warm_up_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.action_export)) + } + + Button( + onClick = { + HapticUtil.performUIHaptic(view) + importLauncher.launch(arrayOf("application/json")) + }, + modifier = Modifier.weight(1f), + colors = androidx.compose.material3.ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_arrow_cool_down_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.action_import)) + } + } +} 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 02474c8ff..a5975de12 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 @@ -11,6 +11,8 @@ import com.sameerasw.essentials.domain.model.NotificationLightingSide import com.sameerasw.essentials.domain.model.NotificationLightingStyle import com.sameerasw.essentials.domain.model.TrackedRepo import com.sameerasw.essentials.domain.model.github.GitHubUser +import com.sameerasw.essentials.utils.RootUtils +import com.sameerasw.essentials.utils.ShizukuUtils import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -147,8 +149,16 @@ class SettingsRepository(private val context: Context) { const val KEY_FLASHLIGHT_PULSE_SELECTED_APPS = "flashlight_pulse_selected_apps" const val KEY_FLASHLIGHT_PULSE_SAME_AS_LIGHTING = "flashlight_pulse_same_as_lighting" + const val KEY_BATTERY_NOTIFICATION_ENABLED = "battery_notification_enabled" const val KEY_USER_DICTIONARY_ENABLED = "user_dictionary_enabled" const val KEY_USER_DICT_LAST_UPDATE = "user_dict_last_update" + + const val KEY_FONT_SCALE = "font_scale" + const val KEY_FONT_WEIGHT = "font_weight" + const val KEY_ANIMATOR_DURATION_SCALE = "animator_duration_scale" + const val KEY_TRANSITION_ANIMATION_SCALE = "transition_animation_scale" + const val KEY_WINDOW_ANIMATION_SCALE = "window_animation_scale" + const val KEY_SMALLEST_WIDTH = "smallest_width" } // Observe changes @@ -202,11 +212,13 @@ class SettingsRepository(private val context: Context) { fun getLong(key: String, default: Long = 0L): Long = prefs.getLong(key, default) // General Setters + fun contains(key: String): Boolean = prefs.contains(key) fun putBoolean(key: String, value: Boolean) = prefs.edit().putBoolean(key, value).apply() fun putString(key: String, value: String?) = prefs.edit().putString(key, value).apply() fun putInt(key: String, value: Int) = prefs.edit().putInt(key, value).apply() fun putFloat(key: String, value: Float) = prefs.edit().putFloat(key, value).apply() fun putLong(key: String, value: Long) = prefs.edit().putLong(key, value).apply() + fun remove(key: String) = prefs.edit().remove(key).apply() // Specific Getters with logic from ViewModel @@ -702,4 +714,134 @@ class SettingsRepository(private val context: Context) { fun isUserDictionaryEnabled(): Boolean = getBoolean(KEY_USER_DICTIONARY_ENABLED, false) fun setUserDictionaryEnabled(enabled: Boolean) = putBoolean(KEY_USER_DICTIONARY_ENABLED, enabled) + + fun isBatteryNotificationEnabled(): Boolean = getBoolean(KEY_BATTERY_NOTIFICATION_ENABLED, false) + fun setBatteryNotificationEnabled(enabled: Boolean) = putBoolean(KEY_BATTERY_NOTIFICATION_ENABLED, enabled) + + fun getFontScale(): Float { + return try { + android.provider.Settings.System.getFloat(context.contentResolver, android.provider.Settings.System.FONT_SCALE) + } catch (e: Exception) { + 1.0f + } + } + + fun setFontScale(scale: Float) { + putFloat(KEY_FONT_SCALE, scale) + try { + android.provider.Settings.System.putFloat(context.contentResolver, android.provider.Settings.System.FONT_SCALE, scale) + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun getFontWeight(): Int { + return try { + android.provider.Settings.Secure.getInt(context.contentResolver, "font_weight_adjustment") + } catch (e: Exception) { + 0 + } + } + + fun setFontWeight(weight: Int) { + putInt(KEY_FONT_WEIGHT, weight) + try { + android.provider.Settings.Secure.putInt(context.contentResolver, "font_weight_adjustment", weight) + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun getSmallestWidth(): Int { + val forcedDensity = try { + android.provider.Settings.Secure.getInt(context.contentResolver, "display_density_forced") + } catch (e: Exception) { + 0 + } + if (forcedDensity > 0) { + val metrics = context.resources.displayMetrics + val widthPx = Math.min(metrics.widthPixels, metrics.heightPixels) + return (widthPx * 160) / forcedDensity + } + return context.resources.configuration.smallestScreenWidthDp + } + + fun setSmallestWidth(widthDp: Int) { + putInt(KEY_SMALLEST_WIDTH, widthDp) + val metrics = context.resources.displayMetrics + val widthPx = Math.min(metrics.widthPixels, metrics.heightPixels) + val density = (widthPx * 160) / widthDp + + val command = "wm density $density" + if (ShizukuUtils.isShizukuAvailable() && ShizukuUtils.hasPermission()) { + ShizukuUtils.runCommand(command) + } else if (RootUtils.isRootAvailable()) { + RootUtils.runCommand(command) + } else { + try { + android.provider.Settings.Secure.putInt(context.contentResolver, "display_density_forced", density) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + fun resetSmallestWidth() { + val command = "wm density reset" + if (ShizukuUtils.isShizukuAvailable() && ShizukuUtils.hasPermission()) { + ShizukuUtils.runCommand(command) + } else if (RootUtils.isRootAvailable()) { + RootUtils.runCommand(command) + } else { + try { + android.provider.Settings.Secure.putString(context.contentResolver, "display_density_forced", null) + } catch (e: Exception) { + e.printStackTrace() + } + } + remove(KEY_SMALLEST_WIDTH) + } + + fun getAnimationScale(key: String): Float { + return try { + android.provider.Settings.Global.getFloat(context.contentResolver, key) + } catch (e: Exception) { + 1.0f + } + } + + fun setAnimationScale(key: String, scale: Float) { + when (key) { + android.provider.Settings.Global.ANIMATOR_DURATION_SCALE -> putFloat(KEY_ANIMATOR_DURATION_SCALE, scale) + android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE -> putFloat(KEY_TRANSITION_ANIMATION_SCALE, scale) + android.provider.Settings.Global.WINDOW_ANIMATION_SCALE -> putFloat(KEY_WINDOW_ANIMATION_SCALE, scale) + } + try { + android.provider.Settings.Global.putFloat(context.contentResolver, key, scale) + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun syncSystemSettingsWithSaved() { + try { + if (contains(KEY_FONT_SCALE)) { + setFontScale(getFloat(KEY_FONT_SCALE, 1.0f)) + } + if (contains(KEY_FONT_WEIGHT)) { + setFontWeight(getInt(KEY_FONT_WEIGHT, 0)) + } + if (contains(KEY_ANIMATOR_DURATION_SCALE)) { + setAnimationScale(android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, getFloat(KEY_ANIMATOR_DURATION_SCALE, 1.0f)) + } + if (contains(KEY_TRANSITION_ANIMATION_SCALE)) { + setAnimationScale(android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE, getFloat(KEY_TRANSITION_ANIMATION_SCALE, 1.0f)) + } + if (contains(KEY_WINDOW_ANIMATION_SCALE)) { + setAnimationScale(android.provider.Settings.Global.WINDOW_ANIMATION_SCALE, getFloat(KEY_WINDOW_ANIMATION_SCALE, 1.0f)) + } + } catch (e: Exception) { + e.printStackTrace() + } + } } diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt index ad2eb3379..6f7f1bbbf 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt @@ -65,5 +65,18 @@ sealed interface Action { override val isConfigurable: Boolean = true } + enum class SoundModeType { + SOUND, VIBRATE, SILENT + } + data class SoundMode(val mode: SoundModeType = SoundModeType.SOUND) : Action { + override val title: Int get() = R.string.diy_action_sound_mode + override val icon: Int get() = when (mode) { + SoundModeType.SOUND -> R.drawable.rounded_volume_up_24 + SoundModeType.VIBRATE -> R.drawable.rounded_mobile_vibrate_24 + SoundModeType.SILENT -> R.drawable.rounded_volume_off_24 + } + override val permissions: List = listOf("notification_policy") + override val isConfigurable: Boolean = true + } } diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt index c9336215c..e9a00e9b5 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt @@ -150,6 +150,21 @@ object FeatureRegistry { override fun isEnabled(viewModel: MainViewModel) = true override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} }, + + object : Feature( + id = "Text and animations", + title = R.string.feat_text_animations_title, + iconRes = R.drawable.rounded_mobile_text_24, + category = R.string.cat_interface, + description = R.string.feat_text_animations_desc, + aboutDescription = R.string.about_desc_text_animations, + permissionKeys = listOf("WRITE_SETTINGS", "WRITE_SECURE_SETTINGS"), + showToggle = false, + parentFeatureId = "Display" + ) { + override fun isEnabled(viewModel: MainViewModel) = true + override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} + }, object : Feature( id = "Watch", title = R.string.feat_watch_title, @@ -407,6 +422,26 @@ object FeatureRegistry { context = context ) }, + object : Feature( + id = "Battery notification", + title = R.string.feat_battery_notification_title, + iconRes = R.drawable.rounded_battery_charging_60_24, + category = R.string.cat_system, + description = R.string.feat_battery_notification_desc, + aboutDescription = R.string.about_desc_battery_notification, + permissionKeys = listOf("POST_NOTIFICATIONS", "BLUETOOTH_CONNECT", "BLUETOOTH_SCAN"), + showToggle = true, + parentFeatureId = "Notifications" + ) { + override fun isEnabled(viewModel: MainViewModel) = + viewModel.isBatteryNotificationEnabled.value + + override fun isToggleEnabled(viewModel: MainViewModel, context: Context) = + viewModel.isPostNotificationsEnabled.value && viewModel.isBluetoothPermissionGranted.value + + override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) = + viewModel.setBatteryNotificationEnabled(enabled, context) + }, object : Feature( id = "Quick settings tiles", diff --git a/app/src/main/java/com/sameerasw/essentials/ime/EssentialsInputMethodService.kt b/app/src/main/java/com/sameerasw/essentials/ime/EssentialsInputMethodService.kt index 74a55c3cb..bb9d8146b 100644 --- a/app/src/main/java/com/sameerasw/essentials/ime/EssentialsInputMethodService.kt +++ b/app/src/main/java/com/sameerasw/essentials/ime/EssentialsInputMethodService.kt @@ -449,9 +449,12 @@ class EssentialsInputMethodService : InputMethodService(), LifecycleOwner, ViewM val ic = currentInputConnection undoRedoManager.undo(ic) }, - onKeyPress = { keyCode -> - handleKeyPress(keyCode) - }, + onKeyPress = { keyCode -> + handleKeyPress(keyCode) + }, + canDelete = { + currentInputConnection?.getTextBeforeCursor(1, 0)?.isNotEmpty() == true + }, onCursorMove = { keyCode, isSelection, isWordJump -> val ic = currentInputConnection if (ic != null) { diff --git a/app/src/main/java/com/sameerasw/essentials/services/BatteryNotificationService.kt b/app/src/main/java/com/sameerasw/essentials/services/BatteryNotificationService.kt new file mode 100644 index 000000000..f2741798b --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/BatteryNotificationService.kt @@ -0,0 +1,185 @@ +package com.sameerasw.essentials.services + +import android.app.* +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils +import com.sameerasw.essentials.R +import com.sameerasw.essentials.data.repository.SettingsRepository +import com.sameerasw.essentials.utils.BatteryRingDrawer +import com.sameerasw.essentials.utils.BluetoothBatteryUtils +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +class BatteryNotificationService : Service() { + + private lateinit var settingsRepository: SettingsRepository + private val NOTIF_ID = 8822 + private val CHANNEL_ID = "battery_notification_channel" + + private val preferenceChangeListener = object : SharedPreferences.OnSharedPreferenceChangeListener { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + if (key == SettingsRepository.KEY_AIRSYNC_MAC_CONNECTED || + key == SettingsRepository.KEY_MAC_BATTERY_LEVEL || + key == SettingsRepository.KEY_MAC_BATTERY_IS_CHARGING || + key == SettingsRepository.KEY_BLUETOOTH_DEVICES_BATTERY || + key == SettingsRepository.KEY_SHOW_BLUETOOTH_DEVICES + ) { + updateNotification() + } + } + } + + override fun onCreate() { + super.onCreate() + settingsRepository = SettingsRepository(this) + createNotificationChannel() + settingsRepository.registerOnSharedPreferenceChangeListener(preferenceChangeListener) + updateNotification() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + updateNotification() + return START_STICKY + } + + override fun onDestroy() { + try { + settingsRepository.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener) + } catch (_: Exception) {} + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? = null + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = getString(R.string.battery_notification_channel_name) + val descriptionText = getString(R.string.battery_notification_channel_desc) + val importance = NotificationManager.IMPORTANCE_LOW + val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { + description = descriptionText + setShowBadge(false) + } + val notificationManager: NotificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + } + + private fun updateNotification() { + val batteryItems = fetchBatteryData() + if (batteryItems.isEmpty()) { + stopForeground(true) + stopSelf() + return + } + + val bitmap = createCompositeBitmap(batteryItems) + val notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.rounded_battery_charging_60_24) + .setLargeIcon(bitmap) + .setStyle(NotificationCompat.BigPictureStyle() + .bigPicture(bitmap) + .bigLargeIcon(null as Bitmap?)) + .setContentTitle(getString(R.string.feat_batteries_title)) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setOngoing(true) + .setSilent(true) + .build() + + startForeground(NOTIF_ID, notification) + } + + private fun fetchBatteryData(): List { + val items = mutableListOf() + val maxDevices = settingsRepository.getBatteryWidgetMaxDevices() + + // Mac + val isAirSyncEnabled = settingsRepository.getBoolean(SettingsRepository.KEY_AIRSYNC_CONNECTION_ENABLED) + val macLevel = settingsRepository.getInt(SettingsRepository.KEY_MAC_BATTERY_LEVEL, -1) + val isMacConnected = settingsRepository.getBoolean(SettingsRepository.KEY_AIRSYNC_MAC_CONNECTED) + val macIsCharging = settingsRepository.getBoolean(SettingsRepository.KEY_MAC_BATTERY_IS_CHARGING) + + if (isAirSyncEnabled && macLevel != -1 && isMacConnected) { + val statusIcon = if (macIsCharging) R.drawable.rounded_flash_on_24 + else if (macLevel <= 15) R.drawable.rounded_battery_android_frame_alert_24 + else null + items.add(BatteryItemData(macLevel, R.drawable.rounded_laptop_mac_24, "Mac", statusIcon)) + } + + // Bluetooth + val isShowBluetoothEnabled = settingsRepository.getBoolean(SettingsRepository.KEY_SHOW_BLUETOOTH_DEVICES) + val bluetoothJson = settingsRepository.getString(SettingsRepository.KEY_BLUETOOTH_DEVICES_BATTERY) + + if (isShowBluetoothEnabled && !bluetoothJson.isNullOrEmpty() && bluetoothJson != "[]") { + try { + val type = object : TypeToken>() {}.type + val devices: List = Gson().fromJson(bluetoothJson, type) ?: emptyList() + devices.forEach { device -> + val iconRes = when { + device.name.contains("watch", true) || device.name.contains("gear", true) || device.name.contains("fit", true) -> R.drawable.rounded_watch_24 + device.name.contains("bud", true) || device.name.contains("pod", true) || device.name.contains("head", true) || device.name.contains("audio", true) || device.name.contains("sound", true) -> R.drawable.rounded_headphones_24 + else -> R.drawable.rounded_bluetooth_24 + } + val statusIcon = if (device.level <= 15) R.drawable.rounded_battery_android_frame_alert_24 else null + items.add(BatteryItemData(device.level, iconRes, device.name, statusIcon)) + } + } catch (_: Exception) {} + } + + return items.take(maxDevices) + } + + private fun createCompositeBitmap(items: List): Bitmap { + val itemSize = 256 + val spacing = 48 + val totalWidth = items.size * itemSize + (items.size - 1) * spacing + val totalHeight = itemSize + + val composite = Bitmap.createBitmap(totalWidth, totalHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(composite) + + val accentColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + getColor(android.R.color.system_accent1_100) + } else { + Color.parseColor("#6200EE") + } + + val onSurface = Color.WHITE + val trackColor = ColorUtils.setAlphaComponent(onSurface, 40) + val surfaceColor = Color.parseColor("#99000000") + + items.forEachIndexed { index, item -> + val ringColor = when { + item.level <= 15 -> Color.parseColor("#F44336") // Red + item.level <= 30 -> Color.parseColor("#FF9800") // Orange + else -> accentColor + } + val itemBitmap = BatteryRingDrawer.drawBatteryWidget( + this, item.level, ringColor, trackColor, onSurface, surfaceColor, + ContextCompat.getDrawable(this, item.iconRes), + item.statusIconRes?.let { ContextCompat.getDrawable(this, it) }, + itemSize, itemSize + ) + canvas.drawBitmap(itemBitmap, (index * (itemSize + spacing)).toFloat(), 0f, null) + } + + return composite + } + + data class BatteryItemData( + val level: Int, + val iconRes: Int, + val name: String, + val statusIconRes: Int? = null + ) +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/CalendarSyncManager.kt b/app/src/main/java/com/sameerasw/essentials/services/CalendarSyncManager.kt index 8ab210ef0..133a0fb1a 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/CalendarSyncManager.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/CalendarSyncManager.kt @@ -1,6 +1,7 @@ package com.sameerasw.essentials.services import android.content.Context +import android.content.SharedPreferences import android.database.ContentObserver import android.os.Handler import android.os.Looper @@ -32,20 +33,22 @@ object CalendarSyncManager { } // Listen for preference changes to start/stop sync - repo.registerOnSharedPreferenceChangeListener { _, key -> - if (key == SettingsRepository.KEY_CALENDAR_SYNC_ENABLED) { - val enabled = repo.getBoolean(key, false) - if (enabled != isSyncEnabled) { - isSyncEnabled = enabled - if (isSyncEnabled) { - startSync(context) - forceSync(context) - } else { - stopSync(context) + repo.registerOnSharedPreferenceChangeListener(object : SharedPreferences.OnSharedPreferenceChangeListener { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + if (key == SettingsRepository.KEY_CALENDAR_SYNC_ENABLED) { + val enabled = repo.getBoolean(key, false) + if (enabled != isSyncEnabled) { + isSyncEnabled = enabled + if (isSyncEnabled) { + startSync(context) + forceSync(context) + } else { + stopSync(context) + } } } } - } + }) } private fun startSync(context: Context) { diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt index 845d77d74..a748351d1 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt @@ -2,6 +2,7 @@ package com.sameerasw.essentials.services.automation.executors import android.content.Context import android.hardware.camera2.CameraManager +import android.media.AudioManager import android.os.Build import com.sameerasw.essentials.domain.diy.Action @@ -174,6 +175,20 @@ object CombinedActionExecutor { } } } + + is Action.SoundMode -> { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val ringerMode = when (action.mode) { + Action.SoundModeType.SOUND -> AudioManager.RINGER_MODE_NORMAL + Action.SoundModeType.VIBRATE -> AudioManager.RINGER_MODE_VIBRATE + Action.SoundModeType.SILENT -> AudioManager.RINGER_MODE_SILENT + } + try { + audioManager.ringerMode = ringerMode + } catch (e: Exception) { + e.printStackTrace() + } + } } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/BatteryNotificationTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/BatteryNotificationTileService.kt new file mode 100644 index 000000000..bc14280e7 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/BatteryNotificationTileService.kt @@ -0,0 +1,50 @@ +package com.sameerasw.essentials.services.tiles + +import android.content.Intent +import android.os.Build +import android.service.quicksettings.Tile +import android.service.quicksettings.TileService +import com.sameerasw.essentials.data.repository.SettingsRepository +import com.sameerasw.essentials.services.BatteryNotificationService + +class BatteryNotificationTileService : TileService() { + + private lateinit var settingsRepository: SettingsRepository + + override fun onCreate() { + super.onCreate() + settingsRepository = SettingsRepository(this) + } + + override fun onStartListening() { + super.onStartListening() + updateTile() + } + + override fun onClick() { + super.onClick() + val newState = !settingsRepository.isBatteryNotificationEnabled() + settingsRepository.setBatteryNotificationEnabled(newState) + + val intent = Intent(this, BatteryNotificationService::class.java) + if (newState) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } + } else { + stopService(intent) + } + + updateTile() + } + + private fun updateTile() { + val tile = qsTile ?: return + val isEnabled = settingsRepository.isBatteryNotificationEnabled() + + tile.state = if (isEnabled) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE + tile.updateTile() + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/PrivateDnsTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/PrivateDnsTileService.kt index d7d29c709..978bb83e9 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/tiles/PrivateDnsTileService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/PrivateDnsTileService.kt @@ -49,7 +49,11 @@ class PrivateDnsTileService : BaseTileService() { } override fun getTileIcon(): Icon { - return Icon.createWithResource(this, R.drawable.rounded_dns_24) + return when (getPrivateDnsMode()) { + MODE_AUTO -> Icon.createWithResource(this, R.drawable.router_24px) + MODE_OFF -> Icon.createWithResource(this, R.drawable.router_off_24px) + else -> Icon.createWithResource(this, R.drawable.router_24px_filled) + } } override fun getTileState(): Int { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt index 5e45315cd..e36b67ac5 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt @@ -73,6 +73,7 @@ import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem import com.sameerasw.essentials.ui.components.pickers.SegmentedPicker import com.sameerasw.essentials.ui.components.sheets.DimWallpaperSettingsSheet +import com.sameerasw.essentials.ui.components.sheets.SoundModeSettingsSheet import com.sameerasw.essentials.ui.theme.EssentialsTheme import com.sameerasw.essentials.utils.AppUtil import com.sameerasw.essentials.utils.HapticUtil @@ -224,6 +225,7 @@ class AutomationEditorActivity : ComponentActivity() { // Config Sheets var showDimSettings by remember { mutableStateOf(false) } var showDeviceEffectsSettings by remember { mutableStateOf(false) } + var showSoundModeSettings by remember { mutableStateOf(false) } var configAction by remember { mutableStateOf(null) } // Generic config action // Validation @@ -527,7 +529,8 @@ class AutomationEditorActivity : ComponentActivity() { Action.TurnOffFlashlight, Action.ToggleFlashlight, Action.HapticVibration, - Action.DimWallpaper() + Action.DimWallpaper(), + Action.SoundMode() ) // Only show Device Effects on Android 15+ actions.add(Action.DeviceEffects()) @@ -600,6 +603,8 @@ class AutomationEditorActivity : ComponentActivity() { showDimSettings = true } else if (resolvedAction is Action.DeviceEffects) { showDeviceEffectsSettings = true + } else if (resolvedAction is Action.SoundMode) { + showSoundModeSettings = true } } ) @@ -647,6 +652,24 @@ class AutomationEditorActivity : ComponentActivity() { ) } + if (showSoundModeSettings && configAction is Action.SoundMode) { + SoundModeSettingsSheet( + initialAction = configAction as Action.SoundMode, + onDismiss = { showSoundModeSettings = false }, + onSave = { newAction -> + showSoundModeSettings = false + when (automationType) { + Automation.Type.TRIGGER -> selectedAction = newAction + Automation.Type.STATE, Automation.Type.APP -> { + if (selectedActionTab == 0) selectedInAction = newAction + else selectedOutAction = newAction + } + } + configAction = null + } + ) + } + // Bottom Actions Row( modifier = Modifier diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/PrivateDnsSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/PrivateDnsSettingsActivity.kt new file mode 100644 index 000000000..5c6494f96 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/PrivateDnsSettingsActivity.kt @@ -0,0 +1,320 @@ +package com.sameerasw.essentials.ui.activities + +import android.content.Context +import android.os.Bundle +import android.provider.Settings +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.theme.EssentialsTheme +import com.sameerasw.essentials.utils.HapticUtil + +class PrivateDnsSettingsActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + val viewModel: com.sameerasw.essentials.viewmodels.MainViewModel = + androidx.lifecycle.viewmodel.compose.viewModel() + val context = LocalContext.current + LaunchedEffect(Unit) { + viewModel.check(context) + } + val isPitchBlackThemeEnabled by viewModel.isPitchBlackThemeEnabled + EssentialsTheme(pitchBlackTheme = isPitchBlackThemeEnabled) { + PrivateDnsSettingsOverlay(onDismiss = { finish() }) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PrivateDnsSettingsOverlay(onDismiss: () -> Unit) { + val context = LocalContext.current + val view = LocalView.current + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + val PRIVATE_DNS_MODE = "private_dns_mode" + val PRIVATE_DNS_SPECIFIER = "private_dns_specifier" + + val currentMode = remember { + Settings.Global.getString(context.contentResolver, PRIVATE_DNS_MODE) ?: "off" + } + val currentHostname = remember { + Settings.Global.getString(context.contentResolver, PRIVATE_DNS_SPECIFIER) ?: "" + } + + var selectedMode by remember { mutableStateOf(currentMode) } + var customHostname by remember { mutableStateOf(currentHostname) } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Header + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.router_24px), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(32.dp) + ) + Text( + text = stringResource(R.string.tile_private_dns), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold + ) + } + + // Mode Selection Container + RoundedCardContainer { + DnsSegmentedItem( + label = stringResource(R.string.tile_private_dns_off), + isSelected = selectedMode == "off", + onClick = { + selectedMode = "off" + HapticUtil.performUIHaptic(view) + } + ) + DnsSegmentedItem( + label = stringResource(R.string.tile_private_dns_auto), + isSelected = selectedMode == "opportunistic", + onClick = { + selectedMode = "opportunistic" + HapticUtil.performUIHaptic(view) + } + ) + DnsSegmentedItem( + label = stringResource(R.string.private_dns_custom_title), + isSelected = selectedMode == "hostname", + onClick = { + selectedMode = "hostname" + HapticUtil.performUIHaptic(view) + } + ) + } + + if (selectedMode == "hostname") { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ) + ) { + OutlinedTextField( + value = customHostname, + onValueChange = { customHostname = it }, + label = { Text(stringResource(R.string.private_dns_hostname_label)) }, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + singleLine = true, + shape = RoundedCornerShape(16.dp) + ) + } + + Text( + text = stringResource(R.string.private_dns_presets_title), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(horizontal = 16.dp), + color = MaterialTheme.colorScheme.primary + ) + + RoundedCardContainer { + val presets = listOf( + Pair(R.string.dns_preset_adguard, R.string.dns_preset_adguard_hostname), + Pair(R.string.dns_preset_google, R.string.dns_preset_google_hostname), + Pair(R.string.dns_preset_cloudflare, R.string.dns_preset_cloudflare_hostname), + Pair(R.string.dns_preset_quad9, R.string.dns_preset_quad9_hostname), + Pair(R.string.dns_preset_cleanbrowsing, R.string.dns_preset_cleanbrowsing_hostname) + ) + + presets.forEach { (nameRes, hostRes) -> + val host = stringResource(hostRes) + DnsPresetItem( + name = stringResource(nameRes), + hostname = host, + isSelected = customHostname == host, + onClick = { + customHostname = host + HapticUtil.performUIHaptic(view) + } + ) + } + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 24.dp, top = 12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedButton( + onClick = onDismiss, + modifier = Modifier + .weight(1f) + .height(56.dp), + shape = RoundedCornerShape(24.dp) + ) { + Text(stringResource(R.string.action_cancel)) + } + + Button( + onClick = { + try { + Settings.Global.putString( + context.contentResolver, + PRIVATE_DNS_MODE, + selectedMode + ) + if (selectedMode == "hostname") { + Settings.Global.putString( + context.contentResolver, + PRIVATE_DNS_SPECIFIER, + customHostname + ) + } + HapticUtil.performHeavyHaptic(view) + onDismiss() + } catch (e: Exception) { + // Handle permission error if any + } + }, + modifier = Modifier + .weight(1f) + .height(56.dp), + shape = RoundedCornerShape(24.dp) + ) { + Text(stringResource(R.string.action_save)) + } + } + } + } +} + +@Composable +fun DnsSegmentedItem( + label: String, + isSelected: Boolean, + onClick: () -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth().clickable { onClick() }, + shape = MaterialTheme.shapes.extraSmall, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ) + ) { + Row( + modifier = Modifier + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton(selected = isSelected, onClick = onClick) + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(start = 12.dp) + ) + } + } +} + +@Composable +fun DnsPresetItem( + name: String, + hostname: String, + isSelected: Boolean, + onClick: () -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth().clickable { onClick() }, + shape = MaterialTheme.shapes.extraSmall, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ) + ) { + Column( + modifier = Modifier + .padding(16.dp) + ) { + Text( + text = name, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface + ) + Text( + text = hostname, + style = MaterialTheme.typography.bodySmall, + color = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/QSPreferencesActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/QSPreferencesActivity.kt index 4c3f4465b..f7c4c2822 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/QSPreferencesActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/QSPreferencesActivity.kt @@ -42,6 +42,15 @@ class QSPreferencesActivity : ComponentActivity() { return } + if (componentName.className == "com.sameerasw.essentials.services.tiles.PrivateDnsTileService") { + val intent = Intent(this, PrivateDnsSettingsActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + startActivity(intent) + finish() + return + } + if (componentName.className == "com.sameerasw.essentials.services.tiles.AdaptiveBrightnessTileService") { val displayIntent = Intent(Settings.ACTION_DISPLAY_SETTINGS).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP @@ -63,6 +72,9 @@ class QSPreferencesActivity : ComponentActivity() { "com.sameerasw.essentials.services.tiles.NfcTileService" -> "NFC" "com.sameerasw.essentials.services.tiles.AdaptiveBrightnessTileService" -> "Quick settings tiles" "com.sameerasw.essentials.services.tiles.MapsPowerSavingTileService" -> "Maps power saving mode" + "com.sameerasw.essentials.services.tiles.UsbDebuggingTileService" -> "Quick settings tiles" + "com.sameerasw.essentials.services.tiles.DeveloperOptionsTileService" -> "Quick settings tiles" + "com.sameerasw.essentials.services.tiles.BatteryNotificationTileService" -> "Battery notification" else -> null } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/AddRepoBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/AddRepoBottomSheet.kt index 5eebfd0a0..b9379703b 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/AddRepoBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/AddRepoBottomSheet.kt @@ -93,6 +93,18 @@ fun AddRepoBottomSheet( } } + val shouldDismiss by viewModel.shouldDismissSheet + LaunchedEffect(shouldDismiss) { + if (shouldDismiss) { + errorMessage?.let { + android.widget.Toast.makeText(context, it, android.widget.Toast.LENGTH_LONG).show() + viewModel.clearError() + } + onDismissRequest() + viewModel.consumeDismissSignal() + } + } + if (showReleaseNotes && latestRelease != null) { val updateInfo = UpdateInfo( versionName = latestRelease!!.tagName, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/SoundModeSettingsSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/SoundModeSettingsSheet.kt new file mode 100644 index 000000000..015dae4e3 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/SoundModeSettingsSheet.kt @@ -0,0 +1,186 @@ +package com.sameerasw.essentials.ui.components.sheets + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.domain.diy.Action +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.utils.ColorUtil +import com.sameerasw.essentials.utils.HapticUtil + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SoundModeSettingsSheet( + initialAction: Action.SoundMode, + onDismiss: () -> Unit, + onSave: (Action.SoundMode) -> Unit +) { + val view = LocalView.current + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + var selectedMode by remember { mutableStateOf(initialAction.mode) } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + dragHandle = null + ) { + Column( + modifier = Modifier + .padding(24.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + Text( + text = stringResource(R.string.diy_action_sound_mode), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + // Info Card + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + shape = RoundedCornerShape(24.dp) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + val infoTitle = "Sound Mode" + Box( + modifier = Modifier + .size(40.dp) + .background( + color = ColorUtil.getPastelColorFor(infoTitle), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(R.drawable.rounded_info_24), + contentDescription = null, + tint = ColorUtil.getVibrantColorFor(infoTitle), + modifier = Modifier.size(24.dp) + ) + } + Text( + text = stringResource(R.string.diy_sound_mode_desc), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + // Mode Selection + RoundedCardContainer(spacing = 2.dp) { + Action.SoundModeType.entries.forEach { mode -> + val isSelected = selectedMode == mode + val title = when (mode) { + Action.SoundModeType.SOUND -> stringResource(R.string.sound_mode_sound) + Action.SoundModeType.VIBRATE -> stringResource(R.string.sound_mode_vibrate) + Action.SoundModeType.SILENT -> stringResource(R.string.sound_mode_silent) + } + val icon = when (mode) { + Action.SoundModeType.SOUND -> R.drawable.rounded_volume_up_24 + Action.SoundModeType.VIBRATE -> R.drawable.rounded_mobile_vibrate_24 + Action.SoundModeType.SILENT -> R.drawable.rounded_volume_off_24 + } + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .clickable { + HapticUtil.performUIHaptic(view) + selectedMode = mode + } + .background(MaterialTheme.colorScheme.surfaceBright) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + RadioButton( + selected = isSelected, + onClick = { + HapticUtil.performUIHaptic(view) + selectedMode = mode + } + ) + Icon( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f), + color = if (isSelected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + // Buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onDismiss() + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceBright, + contentColor = MaterialTheme.colorScheme.onSurface + ) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_close_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text(stringResource(R.string.action_cancel)) + } + + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onSave(initialAction.copy(mode = selectedMode)) + }, + modifier = Modifier.weight(1f) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_check_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text(stringResource(R.string.action_save)) + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sliders/ConfigSliderItem.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sliders/ConfigSliderItem.kt index 4a526de74..f5eaeba48 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/sliders/ConfigSliderItem.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sliders/ConfigSliderItem.kt @@ -30,7 +30,8 @@ fun ConfigSliderItem( steps: Int = 0, increment: Float = 0.1f, valueFormatter: (Float) -> String = { "%.0f".format(it) }, - onValueChangeFinished: (() -> Unit)? = null + onValueChangeFinished: (() -> Unit)? = null, + enabled: Boolean = true ) { Column( modifier = modifier @@ -44,7 +45,7 @@ fun ConfigSliderItem( Text( text = "$title: ${valueFormatter(value)}", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface + color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f) ) Row( modifier = Modifier.fillMaxWidth(), @@ -59,7 +60,8 @@ fun ConfigSliderItem( onValueChange(newValue.coerceIn(valueRange)) onValueChangeFinished?.invoke() }, - modifier = Modifier.padding(end = 4.dp) + modifier = Modifier.padding(end = 4.dp), + enabled = enabled ) { Icon( painter = painterResource(id = R.drawable.rounded_remove_24), @@ -74,7 +76,8 @@ fun ConfigSliderItem( valueRange = valueRange, steps = steps, onValueChangeFinished = onValueChangeFinished, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), + enabled = enabled ) IconButton( @@ -86,7 +89,8 @@ fun ConfigSliderItem( onValueChange(newValue.coerceIn(valueRange)) onValueChangeFinished?.invoke() }, - modifier = Modifier.padding(start = 4.dp) + modifier = Modifier.padding(start = 4.dp), + enabled = enabled ) { Icon( painter = painterResource(id = R.drawable.rounded_add_24), diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/BatteryNotificationSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/BatteryNotificationSettingsUI.kt new file mode 100644 index 000000000..40d5cee8a --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/BatteryNotificationSettingsUI.kt @@ -0,0 +1,61 @@ +package com.sameerasw.essentials.ui.composables.configs + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +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 +import com.sameerasw.essentials.viewmodels.MainViewModel + +@Composable +fun BatteryNotificationSettingsUI( + viewModel: MainViewModel, + modifier: Modifier = Modifier, + highlightKey: String? = null +) { + val context = LocalContext.current + val view = androidx.compose.ui.platform.LocalView.current + + Column( + modifier = modifier.padding(16.dp) + ) { + RoundedCardContainer { + ListItem( + leadingContent = { + androidx.compose.material3.Icon( + painter = painterResource(id = R.drawable.rounded_battery_charging_60_24), + contentDescription = null + ) + }, + headlineContent = { Text(stringResource(R.string.feat_battery_notification_title)) }, + supportingContent = { Text(stringResource(R.string.feat_battery_notification_desc)) }, + trailingContent = { + Switch( + checked = viewModel.isBatteryNotificationEnabled.value, + onCheckedChange = { enabled -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setBatteryNotificationEnabled(enabled, context) + } + ) + } + ) + } + + Text( + text = "This notification displays battery levels for your connected Mac and Bluetooth devices. You can configure which devices to show in the Battery Widget settings.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(16.dp) + ) + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt index a86521971..142d599db 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt @@ -45,6 +45,7 @@ import com.sameerasw.essentials.services.tiles.AdaptiveBrightnessTileService import com.sameerasw.essentials.services.tiles.AlwaysOnDisplayTileService import com.sameerasw.essentials.services.tiles.AppFreezingTileService import com.sameerasw.essentials.services.tiles.AppLockTileService +import com.sameerasw.essentials.services.tiles.BatteryNotificationTileService import com.sameerasw.essentials.services.tiles.BubblesTileService import com.sameerasw.essentials.services.tiles.CaffeinateTileService import com.sameerasw.essentials.services.tiles.DeveloperOptionsTileService @@ -254,6 +255,13 @@ fun QuickSettingsTilesSettingsUI( DeveloperOptionsTileService::class.java, listOf("WRITE_SECURE_SETTINGS"), R.string.about_desc_developer_options + ), + QSTileInfo( + R.string.feat_battery_notification_title, + R.drawable.rounded_battery_charging_60_24, + BatteryNotificationTileService::class.java, + listOf("POST_NOTIFICATIONS", "BLUETOOTH_CONNECT", "BLUETOOTH_SCAN"), + R.string.feat_battery_notification_desc ) ) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/TextAnimationsSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/TextAnimationsSettingsUI.kt new file mode 100644 index 000000000..08367d76f --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/TextAnimationsSettingsUI.kt @@ -0,0 +1,260 @@ +package com.sameerasw.essentials.ui.composables.configs + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.components.sliders.ConfigSliderItem +import com.sameerasw.essentials.utils.HapticUtil +import com.sameerasw.essentials.viewmodels.MainViewModel + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun TextAnimationsSettingsUI( + viewModel: MainViewModel, + modifier: Modifier = Modifier, + highlightSetting: String? = null +) { + val context = LocalContext.current + val view = LocalView.current + + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Text Section + Text( + text = stringResource(R.string.settings_section_text), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + RoundedCardContainer(spacing = 2.dp) { + ConfigSliderItem( + title = stringResource(R.string.label_font_scale), + value = viewModel.fontScale.floatValue, + onValueChange = { + viewModel.updateFontScale(it) + HapticUtil.performSliderHaptic(view) + }, + onValueChangeFinished = { + viewModel.saveFontScale() + }, + valueRange = 0.25f..5.0f, + steps = 0, + increment = 0.05f, + valueFormatter = { String.format("%.2fx", it) } + ) + + ConfigSliderItem( + title = stringResource(R.string.label_font_weight), + value = viewModel.fontWeight.intValue.toFloat(), + onValueChange = { + viewModel.setFontWeight(it.toInt()) + HapticUtil.performSliderHaptic(view) + }, + valueRange = 0f..500f, + steps = 10, + increment = 10f, + valueFormatter = { it.toInt().toString() } + ) + + Row( + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.surfaceBright, + shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd) + ) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.End + ) { + Button( + onClick = { + viewModel.resetTextToDefault() + HapticUtil.performSliderHaptic(view) + }, + colors = ButtonDefaults.filledTonalButtonColors(), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + ) { + Text( + text = stringResource(R.string.label_reset_default), + style = MaterialTheme.typography.labelSmall + ) + } + } + } + + // Scale Section + Text( + text = stringResource(R.string.settings_section_scale), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + RoundedCardContainer(spacing = 2.dp) { + val isEnabled = viewModel.hasShizukuPermission.value + + ConfigSliderItem( + title = stringResource(R.string.label_smallest_width), + value = viewModel.smallestWidth.intValue.toFloat(), + onValueChange = { + viewModel.updateSmallestWidth(it.toInt()) + HapticUtil.performSliderHaptic(view) + }, + onValueChangeFinished = { + viewModel.saveSmallestWidth() + }, + valueRange = 300f..1000f, + steps = 0, + increment = 10f, + valueFormatter = { String.format("%d dp", it.toInt()) }, + enabled = isEnabled + ) + + Row( + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.surfaceBright, + shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd) + ) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = if (isEnabled) Arrangement.End else Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + if (isEnabled) { + Button( + onClick = { + viewModel.resetScaleToDefault() + HapticUtil.performSliderHaptic(view) + }, + colors = ButtonDefaults.filledTonalButtonColors(), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + ) { + Text( + text = stringResource(R.string.label_reset_default), + style = MaterialTheme.typography.labelSmall + ) + } + } else { + Text( + text = stringResource(R.string.msg_shizuku_permission_required), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.weight(1f).padding(end = 8.dp) + ) + Button( + onClick = { + viewModel.requestShizukuPermission() + HapticUtil.performSliderHaptic(view) + }, + colors = ButtonDefaults.filledTonalButtonColors(), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + ) { + Text( + text = stringResource(R.string.label_grant_permission), + style = MaterialTheme.typography.labelSmall + ) + } + } + } + } + + // Animations Section + Text( + text = stringResource(R.string.settings_section_animations), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + RoundedCardContainer(spacing = 2.dp) { + ConfigSliderItem( + title = stringResource(R.string.label_animator_duration_scale), + value = viewModel.animatorDurationScale.floatValue, + onValueChange = { + viewModel.setAnimationScale(android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, it) + HapticUtil.performSliderHaptic(view) + }, + valueRange = 0f..10f, + steps = 0, + increment = 0.05f, + valueFormatter = { String.format("%.2fx", it) } + ) + + ConfigSliderItem( + title = stringResource(R.string.label_transition_animation_scale), + value = viewModel.transitionAnimationScale.floatValue, + onValueChange = { + viewModel.setAnimationScale(android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE, it) + HapticUtil.performSliderHaptic(view) + }, + valueRange = 0f..10f, + steps = 0, + increment = 0.05f, + valueFormatter = { String.format("%.2fx", it) } + ) + + ConfigSliderItem( + title = stringResource(R.string.label_window_animation_scale), + value = viewModel.windowAnimationScale.floatValue, + onValueChange = { + viewModel.setAnimationScale(android.provider.Settings.Global.WINDOW_ANIMATION_SCALE, it) + HapticUtil.performSliderHaptic(view) + }, + valueRange = 0f..10f, + steps = 0, + increment = 0.05f, + valueFormatter = { String.format("%.2fx", it) } + ) + + Row( + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.surfaceBright, + shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd) + ) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.End + ) { + Button( + onClick = { + viewModel.resetAnimationsToDefault() + HapticUtil.performSliderHaptic(view) + }, + colors = ButtonDefaults.filledTonalButtonColors(), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + ) { + Text( + text = stringResource(R.string.label_reset_default), + style = MaterialTheme.typography.labelSmall + ) + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/ime/KeyboardInputView.kt b/app/src/main/java/com/sameerasw/essentials/ui/ime/KeyboardInputView.kt index 24ee880f1..5b7b4b068 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/ime/KeyboardInputView.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/ime/KeyboardInputView.kt @@ -146,6 +146,8 @@ fun KeyButton( shape: androidx.compose.ui.graphics.Shape, containerColor: androidx.compose.ui.graphics.Color, contentColor: androidx.compose.ui.graphics.Color, + onRepeat: (() -> Unit)? = null, + canRepeat: (() -> Boolean)? = null, content: @Composable () -> Unit ) { val isPressed by interactionSource.collectIsPressedAsState() @@ -173,10 +175,35 @@ fun KeyButton( val press = PressInteraction.Press(offset) scope.launch { interactionSource.emit(press) } onPress() - if (tryAwaitRelease()) { - scope.launch { interactionSource.emit(PressInteraction.Release(press)) } - } else { - scope.launch { interactionSource.emit(PressInteraction.Cancel(press)) } + + var isReleased = false + val repeatJob = if (onRepeat != null) { + scope.launch { + delay(500) + while (!isReleased) { + if (canRepeat?.invoke() != false) { + onRepeat() + delay(50) + } else { + break + } + } + } + } else null + + try { + if (tryAwaitRelease()) { + isReleased = true + repeatJob?.cancel() + scope.launch { interactionSource.emit(PressInteraction.Release(press)) } + } else { + isReleased = true + repeatJob?.cancel() + scope.launch { interactionSource.emit(PressInteraction.Cancel(press)) } + } + } catch (e: Exception) { + isReleased = true + repeatJob?.cancel() } }, onLongPress = { @@ -268,7 +295,8 @@ fun KeyboardInputView( onCursorMove: (Int, Boolean, Boolean) -> Unit = { keyCode, _, _ -> onKeyPress(keyCode) }, onCursorDrag: (Boolean) -> Unit = {}, isLongPressSymbolsEnabled: Boolean = false, - onOpened: Int = 0 + onOpened: Int = 0, + canDelete: () -> Boolean = { true } ) { val view = LocalView.current val viewConfiguration = LocalViewConfiguration.current @@ -619,6 +647,15 @@ fun KeyboardInputView( isSuggestionsCollapsed = false } }, + onRepeat = { + if (desc == "Backspace") { + onKeyPress(android.view.KeyEvent.KEYCODE_DEL) + performLightHaptic() + } + }, + canRepeat = { + if (desc == "Backspace") canDelete() else true + }, onPress = { performLightHaptic() }, interactionSource = fnInteraction, containerColor = if ((desc == "Clipboard" && isClipboardMode) || (desc == "Emoji" && isEmojiMode)) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest, @@ -1022,6 +1059,7 @@ fun KeyboardInputView( label = "cornerRadius" ) var delAccumulatedDx by remember { mutableStateOf(0f) } + var isDraggingDel by remember { mutableStateOf(false) } val delSweepThreshold = 25f val animatedColorDel by animateColorAsState( @@ -1041,7 +1079,12 @@ fun KeyboardInputView( .clip(RoundedCornerShape(animatedRadiusDel)) .pointerInput(Unit) { detectHorizontalDragGestures( - onDragStart = { delAccumulatedDx = 0f }, + onDragStart = { + delAccumulatedDx = 0f + isDraggingDel = true + }, + onDragEnd = { isDraggingDel = false }, + onDragCancel = { isDraggingDel = false }, onHorizontalDrag = { change, dragAmount -> change.consume() delAccumulatedDx += dragAmount @@ -1064,19 +1107,45 @@ fun KeyboardInputView( val press = PressInteraction.Press(offset) performLightHaptic() scope.launch { backspaceInteraction.emit(press) } - if (tryAwaitRelease()) { - scope.launch { - backspaceInteraction.emit( - PressInteraction.Release(press) - ) - } - handleKeyPress(KeyEvent.KEYCODE_DEL) - } else { - scope.launch { - backspaceInteraction.emit( - PressInteraction.Cancel(press) - ) + + var isReleased = false + val repeatJob = scope.launch { + delay(500) + while (!isReleased && !isDraggingDel) { + if (canDelete()) { + handleKeyPress(KeyEvent.KEYCODE_DEL) + performLightHaptic() + delay(50) + } else { + break + } + } + } + + try { + if (tryAwaitRelease()) { + isReleased = true + repeatJob.cancel() + scope.launch { + backspaceInteraction.emit( + PressInteraction.Release(press) + ) + } + if (!isDraggingDel) { + handleKeyPress(KeyEvent.KEYCODE_DEL) + } + } else { + isReleased = true + repeatJob.cancel() + scope.launch { + backspaceInteraction.emit( + PressInteraction.Cancel(press) + ) + } } + } catch (e: Exception) { + isReleased = true + repeatJob.cancel() } } ) diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/AppUpdatesViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/AppUpdatesViewModel.kt index b6571a085..12ae2bbac 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/AppUpdatesViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/AppUpdatesViewModel.kt @@ -20,11 +20,16 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File import java.io.FileOutputStream +import java.io.InputStream +import java.io.OutputStream import java.net.HttpURLConnection import java.net.URL +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken class AppUpdatesViewModel : ViewModel() { private val gitHubRepository = GitHubRepository() + private val gson = Gson() private val _searchQuery = mutableStateOf("") val searchQuery: State = _searchQuery @@ -41,6 +46,9 @@ class AppUpdatesViewModel : ViewModel() { private val _errorMessage = mutableStateOf(null) val errorMessage: State = _errorMessage + private val _shouldDismissSheet = mutableStateOf(false) + val shouldDismissSheet: State = _shouldDismissSheet + private val _readmeContent = mutableStateOf(null) val readmeContent: State = _readmeContent @@ -100,6 +108,11 @@ class AppUpdatesViewModel : ViewModel() { } val (owner, repo) = parts + if (owner.lowercase() == "sameerasw" && repo.lowercase() == "essentials") { + _errorMessage.value = context.getString(R.string.msg_restrict_own_app_repo) + _shouldDismissSheet.value = true + return + } _isSearching.value = true _errorMessage.value = null _searchResult.value = null @@ -276,6 +289,10 @@ class AppUpdatesViewModel : ViewModel() { _errorMessage.value = null } + fun consumeDismissSignal() { + _shouldDismissSheet.value = false + } + fun setAllowPreReleases(allow: Boolean) { _allowPreReleases.value = allow } @@ -496,4 +513,53 @@ class AppUpdatesViewModel : ViewModel() { _installingRepoId.value = null } } -} \ No newline at end of file + + fun exportTrackedRepos(context: Context, outputStream: OutputStream) { + try { + val repos = SettingsRepository(context).getTrackedRepos() + val json = gson.toJson(repos) + outputStream.write(json.toByteArray()) + outputStream.flush() + } catch (e: Exception) { + e.printStackTrace() + } finally { + try { + outputStream.close() + } catch (e: Exception) { + } + } + } + + fun importTrackedRepos(context: Context, inputStream: InputStream): Boolean { + return try { + val json = inputStream.bufferedReader().use { it.readText() } + val type = object : TypeToken>() {}.type + val importedRepos: List = gson.fromJson(json, type) + if (importedRepos.isNotEmpty()) { + val settingsRepo = SettingsRepository(context) + val currentRepos = settingsRepo.getTrackedRepos().toMutableList() + importedRepos.forEach { imported -> + val index = currentRepos.indexOfFirst { it.fullName == imported.fullName } + if (index != -1) { + currentRepos[index] = imported + } else { + currentRepos.add(imported) + } + } + settingsRepo.saveTrackedRepos(currentRepos) + loadTrackedRepos(context) + true + } else { + false + } + } catch (e: Exception) { + e.printStackTrace() + false + } finally { + try { + inputStream.close() + } catch (e: Exception) { + } + } + } + } \ No newline at end of file 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 e86b65c44..709077df5 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -8,6 +8,10 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper import android.provider.CalendarContract import android.provider.Settings import android.view.inputmethod.InputMethodManager @@ -40,6 +44,7 @@ import com.sameerasw.essentials.services.receivers.SecurityDeviceAdminReceiver import com.sameerasw.essentials.services.tiles.ScreenOffAccessibilityService import com.sameerasw.essentials.utils.AppUtil import com.sameerasw.essentials.utils.PermissionUtils +import com.sameerasw.essentials.utils.RootUtils import com.sameerasw.essentials.utils.ShizukuUtils import com.sameerasw.essentials.utils.UpdateNotificationHelper import kotlinx.coroutines.Dispatchers @@ -102,6 +107,7 @@ class MainViewModel : ViewModel() { val isCallVibrationsEnabled = mutableStateOf(false) val isCalendarSyncEnabled = mutableStateOf(false) val isCalendarSyncPeriodicEnabled = mutableStateOf(false) + val isBatteryNotificationEnabled = mutableStateOf(false) data class CalendarAccount( val id: Long, @@ -187,6 +193,13 @@ class MainViewModel : ViewModel() { val batteryWidgetMaxDevices = mutableIntStateOf(8) val isBatteryWidgetBackgroundEnabled = mutableStateOf(true) val isAmbientMusicGlanceDockedModeEnabled = mutableStateOf(false) + val fontScale = mutableFloatStateOf(1.0f) + val fontWeight = mutableIntStateOf(0) + val animatorDurationScale = mutableFloatStateOf(1.0f) + val transitionAnimationScale = mutableFloatStateOf(1.0f) + val windowAnimationScale = mutableFloatStateOf(1.0f) + val smallestWidth = mutableIntStateOf(360) + val hasShizukuPermission = mutableStateOf(false) private var lastUpdateCheckTime: Long = 0 lateinit var settingsRepository: SettingsRepository @@ -195,157 +208,192 @@ class MainViewModel : ViewModel() { val gitHubToken = mutableStateOf(null) + private val contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + uri?.let { + when (it) { + Settings.System.getUriFor(Settings.System.FONT_SCALE) -> { + fontScale.floatValue = settingsRepository.getFontScale() + } + Settings.Secure.getUriFor("font_weight_adjustment") -> { + fontWeight.intValue = settingsRepository.getFontWeight() + } + Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE) -> { + animatorDurationScale.floatValue = settingsRepository.getAnimationScale(Settings.Global.ANIMATOR_DURATION_SCALE) + } + Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE) -> { + transitionAnimationScale.floatValue = settingsRepository.getAnimationScale(Settings.Global.TRANSITION_ANIMATION_SCALE) + } + Settings.Global.getUriFor(Settings.Global.WINDOW_ANIMATION_SCALE) -> { + windowAnimationScale.floatValue = settingsRepository.getAnimationScale(Settings.Global.WINDOW_ANIMATION_SCALE) + } + Settings.Secure.getUriFor("display_density_forced") -> { + smallestWidth.intValue = settingsRepository.getSmallestWidth() + } + } + } + } + } + private val preferenceChangeListener = - android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - // We still use this listener for now, attached via Repository - if (key == null) return@OnSharedPreferenceChangeListener + object : android.content.SharedPreferences.OnSharedPreferenceChangeListener { + override fun onSharedPreferenceChanged(sharedPreferences: android.content.SharedPreferences?, key: String?) { + if (key == null) return - when (key) { - SettingsRepository.KEY_EDGE_LIGHTING_ENABLED -> isNotificationLightingEnabled.value = - settingsRepository.getBoolean(key) + when (key) { + SettingsRepository.KEY_EDGE_LIGHTING_ENABLED -> isNotificationLightingEnabled.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_DYNAMIC_NIGHT_LIGHT_ENABLED -> isDynamicNightLightEnabled.value = - settingsRepository.getBoolean(key) + SettingsRepository.KEY_DYNAMIC_NIGHT_LIGHT_ENABLED -> isDynamicNightLightEnabled.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_SCREEN_LOCKED_SECURITY_ENABLED -> isScreenLockedSecurityEnabled.value = - settingsRepository.getBoolean(key) + SettingsRepository.KEY_SCREEN_LOCKED_SECURITY_ENABLED -> isScreenLockedSecurityEnabled.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_MAPS_POWER_SAVING_ENABLED -> { - isMapsPowerSavingEnabled.value = settingsRepository.getBoolean(key) - MapsState.isEnabled = isMapsPowerSavingEnabled.value - } + SettingsRepository.KEY_MAPS_POWER_SAVING_ENABLED -> { + isMapsPowerSavingEnabled.value = settingsRepository.getBoolean(key) + MapsState.isEnabled = isMapsPowerSavingEnabled.value + } - SettingsRepository.KEY_STATUS_BAR_ICON_CONTROL_ENABLED -> isStatusBarIconControlEnabled.value = - settingsRepository.getBoolean(key) + SettingsRepository.KEY_STATUS_BAR_ICON_CONTROL_ENABLED -> isStatusBarIconControlEnabled.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_BUTTON_REMAP_ENABLED -> isButtonRemapEnabled.value = - settingsRepository.getBoolean(key) + SettingsRepository.KEY_BUTTON_REMAP_ENABLED -> isButtonRemapEnabled.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_APP_LOCK_ENABLED -> isAppLockEnabled.value = - settingsRepository.getBoolean(key) + SettingsRepository.KEY_APP_LOCK_ENABLED -> isAppLockEnabled.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_FREEZE_WHEN_LOCKED_ENABLED -> isFreezeWhenLockedEnabled.value = - settingsRepository.getBoolean(key) + SettingsRepository.KEY_FREEZE_WHEN_LOCKED_ENABLED -> isFreezeWhenLockedEnabled.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_FREEZE_DONT_FREEZE_ACTIVE_APPS -> isFreezeDontFreezeActiveAppsEnabled.value = - settingsRepository.getBoolean(key) + SettingsRepository.KEY_FREEZE_DONT_FREEZE_ACTIVE_APPS -> isFreezeDontFreezeActiveAppsEnabled.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_FREEZE_LOCK_DELAY_INDEX -> freezeLockDelayIndex.intValue = - settingsRepository.getInt(key, 1) + SettingsRepository.KEY_FREEZE_LOCK_DELAY_INDEX -> freezeLockDelayIndex.intValue = + settingsRepository.getInt(key, 1) - SettingsRepository.KEY_FREEZE_AUTO_EXCLUDED_APPS -> { - freezeAutoExcludedApps.value = settingsRepository.getFreezeAutoExcludedApps() - } + SettingsRepository.KEY_FREEZE_AUTO_EXCLUDED_APPS -> { + freezeAutoExcludedApps.value = settingsRepository.getFreezeAutoExcludedApps() + } - SettingsRepository.KEY_USE_ROOT -> isRootEnabled.value = - settingsRepository.getBoolean(key) + SettingsRepository.KEY_USE_ROOT -> isRootEnabled.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_CHECK_PRE_RELEASES_ENABLED -> isPreReleaseCheckEnabled.value = - settingsRepository.getBoolean(key) + SettingsRepository.KEY_CHECK_PRE_RELEASES_ENABLED -> isPreReleaseCheckEnabled.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_DEVELOPER_MODE_ENABLED -> { - isDeveloperModeEnabled.value = settingsRepository.getBoolean(key) - } + SettingsRepository.KEY_DEVELOPER_MODE_ENABLED -> { + isDeveloperModeEnabled.value = settingsRepository.getBoolean(key) + } - SettingsRepository.KEY_PITCH_BLACK_THEME_ENABLED -> isPitchBlackThemeEnabled.value = - settingsRepository.getBoolean(key) + SettingsRepository.KEY_PITCH_BLACK_THEME_ENABLED -> isPitchBlackThemeEnabled.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_KEYBOARD_HEIGHT -> keyboardHeight.floatValue = - settingsRepository.getFloat(key, 54f) + SettingsRepository.KEY_KEYBOARD_HEIGHT -> keyboardHeight.floatValue = + settingsRepository.getFloat(key, 54f) - SettingsRepository.KEY_KEYBOARD_BOTTOM_PADDING -> keyboardBottomPadding.floatValue = - settingsRepository.getFloat(key, 0f) + SettingsRepository.KEY_KEYBOARD_BOTTOM_PADDING -> keyboardBottomPadding.floatValue = + settingsRepository.getFloat(key, 0f) - SettingsRepository.KEY_KEYBOARD_ROUNDNESS -> keyboardRoundness.floatValue = - settingsRepository.getFloat(key, 24f) + SettingsRepository.KEY_KEYBOARD_ROUNDNESS -> keyboardRoundness.floatValue = + settingsRepository.getFloat(key, 24f) - SettingsRepository.KEY_KEYBOARD_HAPTICS_ENABLED -> isKeyboardHapticsEnabled.value = - settingsRepository.getBoolean(key) + SettingsRepository.KEY_KEYBOARD_HAPTICS_ENABLED -> isKeyboardHapticsEnabled.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_KEYBOARD_FUNCTIONS_BOTTOM -> isKeyboardFunctionsBottom.value = - settingsRepository.getBoolean(key) + SettingsRepository.KEY_KEYBOARD_FUNCTIONS_BOTTOM -> isKeyboardFunctionsBottom.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_KEYBOARD_FUNCTIONS_PADDING -> keyboardFunctionsPadding.floatValue = - settingsRepository.getFloat(key, 0f) + SettingsRepository.KEY_KEYBOARD_FUNCTIONS_PADDING -> keyboardFunctionsPadding.floatValue = + settingsRepository.getFloat(key, 0f) - SettingsRepository.KEY_KEYBOARD_HAPTIC_STRENGTH -> keyboardHapticStrength.floatValue = - settingsRepository.getFloat(key, 0.5f) + SettingsRepository.KEY_KEYBOARD_HAPTIC_STRENGTH -> keyboardHapticStrength.floatValue = + settingsRepository.getFloat(key, 0.5f) - SettingsRepository.KEY_KEYBOARD_SHAPE -> keyboardShape.intValue = - settingsRepository.getInt(key, 0) + SettingsRepository.KEY_KEYBOARD_SHAPE -> keyboardShape.intValue = + settingsRepository.getInt(key, 0) - SettingsRepository.KEY_KEYBOARD_ALWAYS_DARK -> isKeyboardAlwaysDark.value = - settingsRepository.getBoolean(key, false) + SettingsRepository.KEY_KEYBOARD_ALWAYS_DARK -> isKeyboardAlwaysDark.value = + settingsRepository.getBoolean(key, false) - SettingsRepository.KEY_KEYBOARD_PITCH_BLACK -> isKeyboardPitchBlack.value = - settingsRepository.getBoolean(key, false) + SettingsRepository.KEY_KEYBOARD_PITCH_BLACK -> isKeyboardPitchBlack.value = + settingsRepository.getBoolean(key, false) - SettingsRepository.KEY_KEYBOARD_CLIPBOARD_ENABLED -> isKeyboardClipboardEnabled.value = - settingsRepository.getBoolean(key, true) + SettingsRepository.KEY_KEYBOARD_CLIPBOARD_ENABLED -> isKeyboardClipboardEnabled.value = + settingsRepository.getBoolean(key, true) - SettingsRepository.KEY_KEYBOARD_LONG_PRESS_SYMBOLS -> isLongPressSymbolsEnabled.value = - settingsRepository.getBoolean(key, false) + SettingsRepository.KEY_KEYBOARD_LONG_PRESS_SYMBOLS -> isLongPressSymbolsEnabled.value = + settingsRepository.getBoolean(key, false) - SettingsRepository.KEY_AIRSYNC_CONNECTION_ENABLED -> isAirSyncConnectionEnabled.value = - settingsRepository.getBoolean(key) + SettingsRepository.KEY_AIRSYNC_CONNECTION_ENABLED -> isAirSyncConnectionEnabled.value = + settingsRepository.getBoolean(key) - SettingsRepository.KEY_MAC_BATTERY_LEVEL -> macBatteryLevel.intValue = - settingsRepository.getInt(key, -1) + SettingsRepository.KEY_MAC_BATTERY_LEVEL -> macBatteryLevel.intValue = + settingsRepository.getInt(key, -1) - SettingsRepository.KEY_MAC_BATTERY_IS_CHARGING -> isMacBatteryCharging.value = - settingsRepository.getBoolean(key, false) + SettingsRepository.KEY_MAC_BATTERY_IS_CHARGING -> isMacBatteryCharging.value = + settingsRepository.getBoolean(key, false) - SettingsRepository.KEY_MAC_BATTERY_LAST_UPDATED -> macBatteryLastUpdated.value = - settingsRepository.getLong(key, 0L) + SettingsRepository.KEY_MAC_BATTERY_LAST_UPDATED -> macBatteryLastUpdated.value = + settingsRepository.getLong(key, 0L) - SettingsRepository.KEY_AIRSYNC_MAC_CONNECTED -> isMacConnected.value = - settingsRepository.getBoolean(key, false) + SettingsRepository.KEY_AIRSYNC_MAC_CONNECTED -> isMacConnected.value = + settingsRepository.getBoolean(key, false) - SettingsRepository.KEY_BATTERY_WIDGET_MAX_DEVICES -> batteryWidgetMaxDevices.intValue = - settingsRepository.getInt(key, 8) + SettingsRepository.KEY_BATTERY_WIDGET_MAX_DEVICES -> batteryWidgetMaxDevices.intValue = + settingsRepository.getInt(key, 8) - SettingsRepository.KEY_SNOOZE_DISCOVERED_CHANNELS, SettingsRepository.KEY_SNOOZE_BLOCKED_CHANNELS -> { - appContext?.let { loadSnoozeChannels(it) } - } + SettingsRepository.KEY_SNOOZE_DISCOVERED_CHANNELS, SettingsRepository.KEY_SNOOZE_BLOCKED_CHANNELS -> { + appContext?.let { loadSnoozeChannels(it) } + } - SettingsRepository.KEY_MAPS_DISCOVERED_CHANNELS, SettingsRepository.KEY_MAPS_DETECTION_CHANNELS -> { - appContext?.let { loadMapsChannels(it) } - } + SettingsRepository.KEY_MAPS_DISCOVERED_CHANNELS, SettingsRepository.KEY_MAPS_DETECTION_CHANNELS -> { + appContext?.let { loadMapsChannels(it) } + } - SettingsRepository.KEY_SNOOZE_HEADS_UP_ENABLED -> { - isSnoozeHeadsUpEnabled.value = settingsRepository.getBoolean(key) - } + SettingsRepository.KEY_SNOOZE_HEADS_UP_ENABLED -> { + isSnoozeHeadsUpEnabled.value = settingsRepository.getBoolean(key) + } - SettingsRepository.KEY_PINNED_FEATURES -> { - pinnedFeatureKeys.value = settingsRepository.getPinnedFeatures() - } + SettingsRepository.KEY_PINNED_FEATURES -> { + pinnedFeatureKeys.value = settingsRepository.getPinnedFeatures() + } - SettingsRepository.KEY_CALL_VIBRATIONS_ENABLED -> { - isCallVibrationsEnabled.value = settingsRepository.getBoolean(key) - } + SettingsRepository.KEY_CALL_VIBRATIONS_ENABLED -> { + isCallVibrationsEnabled.value = settingsRepository.getBoolean(key) + } - SettingsRepository.KEY_LIKE_SONG_TOAST_ENABLED -> { - isLikeSongToastEnabled.value = settingsRepository.getBoolean(key) - } + SettingsRepository.KEY_LIKE_SONG_TOAST_ENABLED -> { + isLikeSongToastEnabled.value = settingsRepository.getBoolean(key) + } - SettingsRepository.KEY_LIKE_SONG_AOD_OVERLAY_ENABLED -> { - isLikeSongAodOverlayEnabled.value = settingsRepository.getBoolean(key) - } + SettingsRepository.KEY_LIKE_SONG_AOD_OVERLAY_ENABLED -> { + isLikeSongAodOverlayEnabled.value = settingsRepository.getBoolean(key) + } - SettingsRepository.KEY_AMBIENT_MUSIC_GLANCE_ENABLED -> { - isAmbientMusicGlanceEnabled.value = settingsRepository.getBoolean(key) - } + SettingsRepository.KEY_AMBIENT_MUSIC_GLANCE_ENABLED -> { + isAmbientMusicGlanceEnabled.value = settingsRepository.getBoolean(key) + } - SettingsRepository.KEY_AMBIENT_MUSIC_GLANCE_DOCKED_MODE -> { - isAmbientMusicGlanceDockedModeEnabled.value = settingsRepository.getBoolean(key) - } + SettingsRepository.KEY_AMBIENT_MUSIC_GLANCE_DOCKED_MODE -> { + isAmbientMusicGlanceDockedModeEnabled.value = settingsRepository.getBoolean(key) + } - SettingsRepository.KEY_CALENDAR_SYNC_ENABLED -> { - isCalendarSyncEnabled.value = settingsRepository.getBoolean(key) - } + SettingsRepository.KEY_CALENDAR_SYNC_ENABLED -> { + isCalendarSyncEnabled.value = settingsRepository.getBoolean(key) + } + + SettingsRepository.KEY_TRACKED_REPOS -> { + appContext?.let { refreshTrackedUpdates(it) } + } - SettingsRepository.KEY_TRACKED_REPOS -> { - appContext?.let { refreshTrackedUpdates(it) } + SettingsRepository.KEY_FONT_SCALE -> fontScale.floatValue = settingsRepository.getFontScale() + SettingsRepository.KEY_FONT_WEIGHT -> fontWeight.intValue = settingsRepository.getFontWeight() + SettingsRepository.KEY_ANIMATOR_DURATION_SCALE -> animatorDurationScale.floatValue = settingsRepository.getAnimationScale(android.provider.Settings.Global.ANIMATOR_DURATION_SCALE) + SettingsRepository.KEY_TRANSITION_ANIMATION_SCALE -> transitionAnimationScale.floatValue = settingsRepository.getAnimationScale(android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE) + SettingsRepository.KEY_WINDOW_ANIMATION_SCALE -> windowAnimationScale.floatValue = settingsRepository.getAnimationScale(android.provider.Settings.Global.WINDOW_ANIMATION_SCALE) + SettingsRepository.KEY_SMALLEST_WIDTH -> smallestWidth.intValue = settingsRepository.getSmallestWidth() } } } @@ -384,6 +432,37 @@ class MainViewModel : ViewModel() { isBluetoothPermissionGranted.value = PermissionUtils.hasBluetoothPermission(context) + context.contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.FONT_SCALE), + false, + contentObserver + ) + context.contentResolver.registerContentObserver( + Settings.Secure.getUriFor("font_weight_adjustment"), + false, + contentObserver + ) + context.contentResolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), + false, + contentObserver + ) + context.contentResolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), + false, + contentObserver + ) + context.contentResolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.WINDOW_ANIMATION_SCALE), + false, + contentObserver + ) + context.contentResolver.registerContentObserver( + Settings.Secure.getUriFor("display_density_forced"), + false, + contentObserver + ) + settingsRepository.registerOnSharedPreferenceChangeListener(preferenceChangeListener) viewModelScope.launch { @@ -395,6 +474,15 @@ class MainViewModel : ViewModel() { isWidgetEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_WIDGET_ENABLED) isStatusBarIconControlEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_STATUS_BAR_ICON_CONTROL_ENABLED) + + fontScale.floatValue = settingsRepository.getFontScale() + fontWeight.intValue = settingsRepository.getFontWeight() + animatorDurationScale.floatValue = settingsRepository.getAnimationScale(android.provider.Settings.Global.ANIMATOR_DURATION_SCALE) + transitionAnimationScale.floatValue = settingsRepository.getAnimationScale(android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE) + windowAnimationScale.floatValue = settingsRepository.getAnimationScale(android.provider.Settings.Global.WINDOW_ANIMATION_SCALE) + smallestWidth.intValue = settingsRepository.getSmallestWidth() + hasShizukuPermission.value = ShizukuUtils.hasPermission() || RootUtils.isRootAvailable() + isMapsPowerSavingEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_MAPS_POWER_SAVING_ENABLED) isNotificationLightingEnabled.value = @@ -627,9 +715,37 @@ class MainViewModel : ViewModel() { isCalendarSyncEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_CALENDAR_SYNC_ENABLED, false) isCalendarSyncPeriodicEnabled.value = settingsRepository.isCalendarSyncPeriodicEnabled() + isBatteryNotificationEnabled.value = settingsRepository.isBatteryNotificationEnabled() selectedCalendarIds.value = settingsRepository.getCalendarSyncSelectedCalendars() refreshTrackedUpdates(context) + if (isBatteryNotificationEnabled.value) { + startBatteryNotificationService(context) + } + } + + private fun startBatteryNotificationService(context: Context) { + val intent = Intent(context, com.sameerasw.essentials.services.BatteryNotificationService::class.java) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + + private fun stopBatteryNotificationService(context: Context) { + val intent = Intent(context, com.sameerasw.essentials.services.BatteryNotificationService::class.java) + context.stopService(intent) + } + + fun setBatteryNotificationEnabled(enabled: Boolean, context: Context) { + isBatteryNotificationEnabled.value = enabled + settingsRepository.setBatteryNotificationEnabled(enabled) + if (enabled) { + startBatteryNotificationService(context) + } else { + stopBatteryNotificationService(context) + } } fun onSearchQueryChanged(query: String, context: Context) { @@ -978,6 +1094,62 @@ class MainViewModel : ViewModel() { settingsRepository.putBoolean(SettingsRepository.KEY_AMBIENT_MUSIC_GLANCE_ENABLED, enabled) } + fun updateFontScale(scale: Float) { + fontScale.floatValue = scale + } + + fun saveFontScale() { + settingsRepository.setFontScale(fontScale.floatValue) + } + + fun setFontScale(scale: Float) { + fontScale.floatValue = scale + settingsRepository.setFontScale(scale) + } + + fun setFontWeight(weight: Int) { + fontWeight.intValue = weight + settingsRepository.setFontWeight(weight) + } + + fun setAnimationScale(key: String, scale: Float) { + when (key) { + android.provider.Settings.Global.ANIMATOR_DURATION_SCALE -> animatorDurationScale.floatValue = scale + android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE -> transitionAnimationScale.floatValue = scale + android.provider.Settings.Global.WINDOW_ANIMATION_SCALE -> windowAnimationScale.floatValue = scale + } + settingsRepository.setAnimationScale(key, scale) + } + + fun resetTextToDefault() { + setFontScale(1.0f) + setFontWeight(0) + } + + fun resetAnimationsToDefault() { + setAnimationScale(android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f) + setAnimationScale(android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE, 1.0f) + setAnimationScale(android.provider.Settings.Global.WINDOW_ANIMATION_SCALE, 1.0f) + } + + fun updateSmallestWidth(width: Int) { + smallestWidth.intValue = width + } + + fun saveSmallestWidth() { + settingsRepository.setSmallestWidth(smallestWidth.intValue) + } + + fun setSmallestWidth(width: Int) { + smallestWidth.intValue = width + settingsRepository.setSmallestWidth(width) + } + + fun resetScaleToDefault() { + settingsRepository.resetSmallestWidth() + smallestWidth.intValue = settingsRepository.getSmallestWidth() + } + fun setAmbientMusicGlanceDockedModeEnabled(enabled: Boolean) { isAmbientMusicGlanceDockedModeEnabled.value = enabled settingsRepository.putBoolean( @@ -1906,6 +2078,7 @@ class MainViewModel : ViewModel() { fun importConfigs(context: Context, inputStream: java.io.InputStream): Boolean { val success = settingsRepository.importConfigs(inputStream) if (success) { + settingsRepository.syncSystemSettingsWithSaved() check(context) } return success @@ -1916,4 +2089,12 @@ class MainViewModel : ViewModel() { val settingsJson = settingsRepository.getAllConfigsAsJsonString() return com.sameerasw.essentials.utils.LogManager.generateReport(context, settingsJson) } + + override fun onCleared() { + super.onCleared() + appContext?.contentResolver?.unregisterContentObserver(contentObserver) + if (::settingsRepository.isInitialized) { + settingsRepository.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener) + } + } } diff --git a/app/src/main/res/drawable/rounded_arrow_cool_down_24.xml b/app/src/main/res/drawable/rounded_arrow_cool_down_24.xml new file mode 100644 index 000000000..7e37f1a68 --- /dev/null +++ b/app/src/main/res/drawable/rounded_arrow_cool_down_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_arrow_warm_up_24.xml b/app/src/main/res/drawable/rounded_arrow_warm_up_24.xml new file mode 100644 index 000000000..d94b36920 --- /dev/null +++ b/app/src/main/res/drawable/rounded_arrow_warm_up_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/router_24px.xml b/app/src/main/res/drawable/router_24px.xml new file mode 100644 index 000000000..32b27bb18 --- /dev/null +++ b/app/src/main/res/drawable/router_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/router_24px_filled.xml b/app/src/main/res/drawable/router_24px_filled.xml new file mode 100644 index 000000000..9c625a572 --- /dev/null +++ b/app/src/main/res/drawable/router_24px_filled.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/router_off_24px.xml b/app/src/main/res/drawable/router_off_24px.xml new file mode 100644 index 000000000..cbb5ac25a --- /dev/null +++ b/app/src/main/res/drawable/router_off_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/twotone_dns_24.xml b/app/src/main/res/drawable/twotone_dns_24.xml new file mode 100644 index 000000000..034dd7b2e --- /dev/null +++ b/app/src/main/res/drawable/twotone_dns_24.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d17cdd5f9..68b0255cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -200,6 +200,19 @@ USB Debugging On Off + Custom Private DNS + Common DNS Presets + Provider hostname + AdGuard DNS + dns.adguard.com + Google Public DNS + dns.google + Cloudflare DNS + 1dot1dot1dot1.cloudflare-dns.com + Quad9 DNS + dns.quad9.net + CleanBrowsing + adult-filter-dns.cleanbrowsing.org Screen locked security @@ -656,6 +669,8 @@ This feature requires Android 15 or higher. Enabled Disabled + Sound Mode + This action allows switching between Sound, Vibrate, and Silent modes based on triggers. It requires Do Not Disturb access. Sameera Wijerathna @@ -1016,6 +1031,14 @@ Download AirSync App Required for Mac battery sync + Battery notification + Persistent battery status notification + Replicate the battery widget experience in your notification shade. It will show the battery levels of all your connected devices in a single persistent notification, updated in real-time. This includes your Mac (via AirSync) and Bluetooth accessories. + Battery Status Notification + Persistent notification showing connected devices battery levels + Nearby Devices + Required to detect and retrieve battery information from Bluetooth accessories + Copy code Open login page @@ -1091,4 +1114,27 @@ Protection ABC ?#/ + Oi! You can check updates in app settings, No need to add here XD + Export + Import + Repositories exported successfully + Failed to export repositories + Repositories imported successfully + Failed to import repositories + Apps + Scale and Animations + Adjust system scale and animations + Text + Font Scale + Font Weight + Reset + Scale + Smallest Width + Shizuku permission required to adjust scale + Grant Permission + Animations + Animator duration scale + Transition animation scale + Window animation scale + Adjust system-wide font scale, weight, and animation speeds. Note that some settings may require advanced permissions or a device reboot for certain apps to reflect changes. \n\nAdditional shizuku or root permission may be necessary for scale adjustments \ No newline at end of file