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