diff --git a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt index 52a5d9425e..3910e5f0b1 100644 --- a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt @@ -70,6 +70,7 @@ import com.sameerasw.essentials.ui.composables.configs.LiveWallpaperSettingsUI import com.sameerasw.essentials.ui.composables.configs.LocationReachedSettingsUI import com.sameerasw.essentials.ui.composables.configs.LockScreenClockSettingsUI import com.sameerasw.essentials.ui.composables.configs.MapsPowerSavingSettingsUI +import com.sameerasw.essentials.ui.composables.configs.NetworksSettingsUI import com.sameerasw.essentials.ui.composables.configs.NotificationLightingSettingsUI import com.sameerasw.essentials.ui.composables.configs.NotificationSnoozingSettingsUI import com.sameerasw.essentials.ui.composables.configs.OtherCustomizationsSettingsUI @@ -83,6 +84,7 @@ import com.sameerasw.essentials.ui.composables.configs.ScreenOffWidgetSettingsUI import com.sameerasw.essentials.ui.composables.configs.ShutUpSettingsUI import com.sameerasw.essentials.ui.composables.configs.SnoozeNotificationsSettingsUI import com.sameerasw.essentials.ui.composables.configs.SoundModeTileSettingsUI +import com.sameerasw.essentials.ui.composables.configs.StandbyAppsSettingsUI import com.sameerasw.essentials.ui.composables.configs.StatusBarIconSettingsUI import com.sameerasw.essentials.ui.composables.configs.TextAnimationsSettingsUI import com.sameerasw.essentials.ui.composables.configs.WatchControlsSettingsUI @@ -337,6 +339,7 @@ class FeatureSettingsActivity : AppCompatActivity() { "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isUsageStatsPermissionGranted.value "Power and Battery" -> !isWriteSecureSettingsEnabled + "Networks" -> !isWriteSecureSettingsEnabled && !com.sameerasw.essentials.utils.ShellUtils.hasPermission(context) else -> false } if (hasMissingPermissions) { @@ -513,7 +516,7 @@ class FeatureSettingsActivity : AppCompatActivity() { context, viewModel.isEnableUnsupportedFeatures.value ).filter { it.parentFeatureId == featureId } - if (children.isNotEmpty()) { + if (children.isNotEmpty() && featureId != "Networks") { RoundedCardContainer( modifier = Modifier .padding(horizontal = 16.dp) @@ -560,6 +563,7 @@ class FeatureSettingsActivity : AppCompatActivity() { "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isUsageStatsPermissionGranted.value "Power and Battery" -> !isWriteSecureSettingsEnabled + "Networks" -> !isWriteSecureSettingsEnabled && !com.sameerasw.essentials.utils.ShellUtils.hasPermission(context) "Disable safe volume warning" -> !isWriteSecureSettingsEnabled "Notification snoozing" -> !isWriteSecureSettingsEnabled else -> false @@ -896,6 +900,21 @@ class FeatureSettingsActivity : AppCompatActivity() { highlightSetting = highlightSetting ) } + + "Networks" -> { + NetworksSettingsUI( + viewModel = viewModel, + modifier = Modifier.padding(top = 16.dp), + highlightSetting = highlightSetting + ) + } + + "Standby apps" -> { + StandbyAppsSettingsUI( + viewModel = viewModel, + modifier = Modifier.padding(top = 16.dp) + ) + } } } diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt index 3f239f206e..1bb92b9d0d 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 @@ -264,6 +264,13 @@ class SettingsRepository(private val context: Context) { const val KEY_SHIZUKU_AUTH_TOKEN = "shizuku_auth_token" const val KEY_EDGE_LIGHTING_SWEEP_SELECTED_SHAPES = "edge_lighting_sweep_selected_shapes" const val KEY_DISABLE_ROTATION_SUGGESTION = "disable_rotation_suggestion" + const val KEY_ALLOW_OVERLAYS_IN_SETTINGS = "allow_overlays_in_settings" + const val KEY_NETWORK_DOWNLOAD_RATE_LIMIT = "network_download_rate_limit" + const val KEY_MOBILE_DATA_ALWAYS_ON = "mobile_data_always_on" + const val KEY_WIRELESS_DISPLAY_CERTIFICATION = "wireless_display_certification" + const val KEY_PREFER_GPU_COMPOSING = "prefer_gpu_composing" + const val KEY_TRANSPARENT_NAVIGATION_BAR = "transparent_navigation_bar" + const val KEY_STANDBY_APPS = "standby_apps" const val KEY_PIXEL_SEARCHBAR = "pixel_searchbar" const val KEY_PIXEL_SEARCHBAR_TYPE = "pixel_searchbar_type" const val KEY_PIXEL_SEARCHBAR_DATE_FORMAT = "pixel_searchbar_date_format" diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/AppStandbyInfo.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/AppStandbyInfo.kt new file mode 100644 index 0000000000..4ecf7987d1 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/domain/model/AppStandbyInfo.kt @@ -0,0 +1,10 @@ +package com.sameerasw.essentials.domain.model + +import android.graphics.drawable.Drawable + +data class AppStandbyInfo( + val packageName: String, + val label: String, + val icon: Drawable?, + val bucket: Int +) 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 77e6be5b3a..016c4d648b 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 @@ -289,6 +289,62 @@ object FeatureRegistry { override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} override fun isDeviceSupported(context: Context) = DeviceUtils.isGoogleDevice() }, + object : Feature( + id = "Networks", + title = R.string.feat_networks_title, + iconRes = R.drawable.rounded_cell_wifi_24, + category = R.string.cat_connectivity, + description = R.string.feat_networks_desc, + aboutDescription = R.string.about_desc_networks, + showToggle = false, + hasMoreSettings = true + ) { + override fun isEnabled(viewModel: MainViewModel) = true + override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} + }, + + object : Feature( + id = "Network download rate limit", + title = R.string.feat_network_download_rate_limit_title, + iconRes = R.drawable.rounded_cell_wifi_24, + category = R.string.cat_connectivity, + description = R.string.feat_network_download_rate_limit_desc, + permissionKeys = listOf("WRITE_SECURE_SETTINGS"), + parentFeatureId = "Networks", + showToggle = false + ) { + override fun isEnabled(viewModel: MainViewModel) = viewModel.networkDownloadRateLimit.intValue != -1 + override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} + }, + + object : Feature( + id = "Mobile data always on", + title = R.string.feat_mobile_data_always_on_title, + iconRes = R.drawable.rounded_mobile_24, + category = R.string.cat_connectivity, + description = R.string.feat_mobile_data_always_on_desc, + permissionKeys = listOf("WRITE_SECURE_SETTINGS"), + parentFeatureId = "Networks" + ) { + override fun isEnabled(viewModel: MainViewModel) = viewModel.isMobileDataAlwaysOnEnabled.value + override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) = + viewModel.setMobileDataAlwaysOnEnabled(enabled, context) + }, + + object : Feature( + id = "Wireless display certification", + title = R.string.feat_wireless_display_certification_title, + iconRes = R.drawable.rounded_cast_24, + category = R.string.cat_connectivity, + description = R.string.feat_wireless_display_certification_desc, + permissionKeys = listOf("WRITE_SECURE_SETTINGS"), + parentFeatureId = "Networks" + ) { + override fun isEnabled(viewModel: MainViewModel) = viewModel.isWirelessDisplayCertificationEnabled.value + override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) = + viewModel.setWirelessDisplayCertificationEnabled(enabled, context) + }, + object : Feature( id = "Watch", title = R.string.feat_watch_title, @@ -607,6 +663,22 @@ object FeatureRegistry { } }, + object : Feature( + id = "Standby apps", + title = R.string.feat_standby_apps_title, + iconRes = R.drawable.rounded_app_registration_24, + category = R.string.cat_interaction, + description = R.string.feat_standby_apps_desc, + aboutDescription = R.string.about_desc_standby_apps, + permissionKeys = listOf("SHIZUKU"), + parentFeatureId = "Input", + showToggle = false, + hasMoreSettings = true + ) { + override fun isEnabled(viewModel: MainViewModel) = true + override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} + }, + object : Feature( id = "Snooze system notifications", title = R.string.feat_snooze_notifications_title, @@ -961,6 +1033,7 @@ object FeatureRegistry { iconRes = R.drawable.rounded_home_24, category = R.string.cat_display, description = R.string.feat_other_customizations_desc, + aboutDescription = R.string.about_desc_other_customizations, showToggle = false, hasMoreSettings = true, parentFeatureId = "Display" @@ -969,6 +1042,8 @@ object FeatureRegistry { override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} }, + + object : Feature( id = "Screen locked security", title = R.string.feat_screen_locked_security_title, diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt index e2c2f44e9e..c0abf8c7c9 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt @@ -30,6 +30,11 @@ fun initPermissionRegistry() { PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.tile_developer_options) PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.tile_charge_optimization) PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.feat_lock_screen_clock_title) + PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.feat_allow_overlays_in_settings_title) + PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.feat_networks_title) + PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.feat_network_download_rate_limit_title) + PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.feat_mobile_data_always_on_title) + PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.feat_wireless_display_certification_title) // Shizuku permission PermissionRegistry.register("SHIZUKU", R.string.feat_freeze_title) @@ -37,6 +42,10 @@ fun initPermissionRegistry() { PermissionRegistry.register("SHIZUKU", R.string.feat_screen_locked_security_title) PermissionRegistry.register("SHIZUKU", R.string.feat_screen_refresh_rate_title) PermissionRegistry.register("SHIZUKU", R.string.tile_refresh_rate) + PermissionRegistry.register("SHIZUKU", R.string.feat_prefer_gpu_composing_title) + PermissionRegistry.register("SHIZUKU", R.string.feat_networks_title) + PermissionRegistry.register("SHIZUKU", R.string.feat_transparent_navigation_bar_title) + PermissionRegistry.register("SHIZUKU", R.string.feat_standby_apps_title) PermissionRegistry.register("USAGE_STATS", R.string.feat_freeze_title) PermissionRegistry.register("USAGE_STATS", R.string.feat_app_lock_title) PermissionRegistry.register("USAGE_STATS", R.string.feat_dynamic_night_light_title) @@ -46,7 +55,11 @@ fun initPermissionRegistry() { PermissionRegistry.register("ROOT", R.string.feat_maps_power_saving_title) PermissionRegistry.register("ROOT", R.string.feat_freeze_title) PermissionRegistry.register("ROOT", R.string.feat_button_remap_title) + PermissionRegistry.register("ROOT", R.string.feat_prefer_gpu_composing_title) + PermissionRegistry.register("ROOT", R.string.feat_transparent_navigation_bar_title) + PermissionRegistry.register("ROOT", R.string.feat_standby_apps_title) PermissionRegistry.register("ROOT", R.string.feat_screen_locked_security_title) + PermissionRegistry.register("ROOT", R.string.feat_prefer_gpu_composing_title) // Notification listener permission PermissionRegistry.register("NOTIFICATION_LISTENER", R.string.feat_maps_power_saving_title) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/NetworksSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/NetworksSettingsUI.kt new file mode 100644 index 0000000000..301bf07845 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/NetworksSettingsUI.kt @@ -0,0 +1,240 @@ +package com.sameerasw.essentials.ui.composables.configs + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import kotlin.math.roundToInt +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.components.cards.IconToggleItem +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.components.sheets.PermissionItem +import com.sameerasw.essentials.ui.components.sheets.PermissionsBottomSheet +import com.sameerasw.essentials.ui.components.sliders.ConfigSliderItem +import com.sameerasw.essentials.ui.modifiers.highlight +import com.sameerasw.essentials.utils.HapticUtil +import com.sameerasw.essentials.viewmodels.MainViewModel + +private enum class NetworkPermissionModule { + RATE_LIMIT, + MOBILE_DATA_ALWAYS_ON, + WIRELESS_DISPLAY_CERTIFICATION, + NONE +} + +@Composable +fun NetworksSettingsUI( + viewModel: MainViewModel, + modifier: Modifier = Modifier, + highlightSetting: String? = null +) { + val context = LocalContext.current + val view = LocalView.current + var requestingPermissionFor by remember { mutableStateOf(NetworkPermissionModule.NONE) } + + val presetValues = remember { + intArrayOf( + -1, // Disabled + 16000, // 128 Kbps + 32000, // 256 Kbps + 64000, // 512 Kbps + 125000, // 1 Mbps + 250000, // 2 Mbps + 625000, // 5 Mbps + 1250000, // 10 Mbps + 1875000, // 15 Mbps + 3125000, // 25 Mbps + 6250000, // 50 Mbps + 12500000, // 100 Mbps + 18750000, // 150 Mbps + 25000000, // 200 Mbps + 31250000 // 250 Mbps + ) + } + val disabledLabel = stringResource(R.string.rate_limit_disabled) + val presetLabels = remember(disabledLabel) { + listOf( + disabledLabel, + "128 Kbps", + "256 Kbps", + "512 Kbps", + "1 Mbps", + "2 Mbps", + "5 Mbps", + "10 Mbps", + "15 Mbps", + "25 Mbps", + "50 Mbps", + "100 Mbps", + "150 Mbps", + "200 Mbps", + "250 Mbps" + ) + } + + val isShizukuAvailable = viewModel.isShizukuAvailable.value + val isShizukuGranted = viewModel.isShizukuPermissionGranted.value + val isRootAvailable = viewModel.isRootAvailable.value + val isRootGranted = viewModel.isRootPermissionGranted.value + val isShellGranted = (isShizukuAvailable && isShizukuGranted) || (isRootAvailable && isRootGranted) + val isHasWritePermission = viewModel.isWriteSecureSettingsEnabled.value || isShellGranted + + LaunchedEffect(Unit) { + viewModel.refreshNetworksState(context) + } + + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + viewModel.refreshNetworksState(context) + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + + if (requestingPermissionFor != NetworkPermissionModule.NONE) { + val shizukuPermission = PermissionItem( + iconRes = R.drawable.rounded_adb_24, + title = if (!isShizukuAvailable) R.string.perm_shizuku_title else R.string.perm_shizuku_grant_title, + description = if (!isShizukuAvailable) R.string.perm_shizuku_desc else R.string.perm_shizuku_grant_desc, + dependentFeatures = listOf( + R.string.feat_network_download_rate_limit_title, + R.string.feat_mobile_data_always_on_title, + R.string.feat_wireless_display_certification_title + ), + actionLabel = if (!isShizukuAvailable) R.string.perm_shizuku_install_action else if (isShellGranted) R.string.perm_action_granted else R.string.perm_action_grant, + action = { + if (!isShizukuAvailable) { + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=moe.shizuku.privileged.api") + ) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + } else { + viewModel.requestShizukuPermission() + } + }, + isGranted = isShellGranted + ) + + PermissionsBottomSheet( + onDismissRequest = { requestingPermissionFor = NetworkPermissionModule.NONE }, + featureTitle = R.string.feat_networks_title, + permissions = listOf(shizukuPermission) + ) + } + + Column( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + RoundedCardContainer( + modifier = Modifier, + spacing = 2.dp, + cornerRadius = 24.dp + ) { + val currentRateLimit = viewModel.networkDownloadRateLimit.intValue + val currentIndex = remember(currentRateLimit) { + val idx = presetValues.indexOf(currentRateLimit) + if (idx != -1) idx + else presetValues.indices.minByOrNull { kotlin.math.abs(presetValues[it] - currentRateLimit) } ?: 0 + } + var sliderValue by remember(currentIndex) { mutableFloatStateOf(currentIndex.toFloat()) } + + ConfigSliderItem( + title = stringResource(R.string.feat_network_download_rate_limit_title), + description = stringResource(R.string.feat_network_download_rate_limit_desc), + value = sliderValue, + onValueChange = { floatVal -> + sliderValue = floatVal + val newIndex = floatVal.roundToInt().coerceIn(0, presetValues.lastIndex) + if (presetValues[newIndex] != currentRateLimit) { + HapticUtil.performSliderHaptic(view) + if (isHasWritePermission) { + viewModel.setNetworkDownloadRateLimit(presetValues[newIndex], context) + } else { + requestingPermissionFor = NetworkPermissionModule.RATE_LIMIT + } + } + }, + valueRange = 0f..(presetValues.lastIndex.toFloat()), + steps = presetValues.size - 2, + increment = 1f, + valueFormatter = { floatVal -> + val idx = floatVal.roundToInt().coerceIn(0, presetLabels.lastIndex) + presetLabels[idx] + }, + iconRes = R.drawable.rounded_cell_wifi_24, + enabled = true, + modifier = Modifier.highlight(highlightSetting == "network_download_rate_limit_slider") + ) + + IconToggleItem( + title = stringResource(R.string.feat_mobile_data_always_on_title), + description = stringResource(R.string.feat_mobile_data_always_on_desc), + isChecked = viewModel.isMobileDataAlwaysOnEnabled.value, + onCheckedChange = { enabled -> + if (isHasWritePermission) { + viewModel.setMobileDataAlwaysOnEnabled(enabled, context) + } else { + requestingPermissionFor = NetworkPermissionModule.MOBILE_DATA_ALWAYS_ON + } + }, + enabled = true, + onDisabledClick = { + if (!isHasWritePermission) { + requestingPermissionFor = NetworkPermissionModule.MOBILE_DATA_ALWAYS_ON + } + }, + iconRes = R.drawable.rounded_mobile_24, + modifier = Modifier.highlight(highlightSetting == "mobile_data_always_on_toggle") + ) + + IconToggleItem( + title = stringResource(R.string.feat_wireless_display_certification_title), + description = stringResource(R.string.feat_wireless_display_certification_desc), + isChecked = viewModel.isWirelessDisplayCertificationEnabled.value, + onCheckedChange = { enabled -> + if (isHasWritePermission) { + viewModel.setWirelessDisplayCertificationEnabled(enabled, context) + } else { + requestingPermissionFor = NetworkPermissionModule.WIRELESS_DISPLAY_CERTIFICATION + } + }, + enabled = true, + onDisabledClick = { + if (!isHasWritePermission) { + requestingPermissionFor = NetworkPermissionModule.WIRELESS_DISPLAY_CERTIFICATION + } + }, + iconRes = R.drawable.rounded_cast_24, + modifier = Modifier.highlight(highlightSetting == "wireless_display_certification_toggle") + ) + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt index 5560f871e5..f20f48ec28 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/OtherCustomizationsSettingsUI.kt @@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue @@ -37,6 +39,9 @@ enum class PermissionModule { CIRCLE_TO_SEARCH, DISABLE_ROTATION_SUGGESTION, PIXEL_SEARCHBAR, + PREFER_GPU_COMPOSING, + ALLOW_OVERLAYS_IN_SETTINGS, + TRANSPARENT_NAVIGATION_BAR, NONE } @@ -67,7 +72,8 @@ fun OtherCustomizationsSettingsUI( dependentFeatures = listOf( R.string.feat_hide_gesture_bar_title, R.string.feat_hide_gesture_bar_on_launcher_title, - R.string.feat_circle_to_search_gesture_title + R.string.feat_circle_to_search_gesture_title, + R.string.feat_prefer_gpu_composing_title ), actionLabel = if (!isShizukuAvailable) R.string.perm_shizuku_install_action else if (isShellGranted) R.string.perm_action_granted else R.string.perm_action_grant, action = { @@ -123,6 +129,9 @@ fun OtherCustomizationsSettingsUI( PermissionModule.CIRCLE_TO_SEARCH -> listOf(shizukuPermission, accessibilityPermission) PermissionModule.DISABLE_ROTATION_SUGGESTION -> listOf(shizukuPermission) PermissionModule.PIXEL_SEARCHBAR -> listOf(shizukuPermission) + PermissionModule.PREFER_GPU_COMPOSING -> listOf(shizukuPermission) + PermissionModule.ALLOW_OVERLAYS_IN_SETTINGS -> listOf(shizukuPermission) + PermissionModule.TRANSPARENT_NAVIGATION_BAR -> listOf(shizukuPermission) else -> emptyList() } @@ -160,6 +169,9 @@ fun OtherCustomizationsSettingsUI( val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { viewModel.setCircleToSearchPreviewEnabled(viewModel.isCircleToSearchGestureEnabled.value) + viewModel.refreshPreferGpuComposingState(context) + viewModel.refreshAllowOverlaysInSettingsState(context) + viewModel.refreshTransparentNavigationBarState(context) } else if (event == Lifecycle.Event.ON_PAUSE) { viewModel.setCircleToSearchPreviewEnabled(false) } @@ -255,7 +267,47 @@ fun OtherCustomizationsSettingsUI( modifier = Modifier.highlight(highlightSetting == "disable_rotation_suggestion_toggle") ) + IconToggleItem( + title = stringResource(R.string.feat_allow_overlays_in_settings_title), + description = stringResource(R.string.feat_allow_overlays_in_settings_desc), + isChecked = viewModel.isAllowOverlaysInSettingsEnabled.value, + onCheckedChange = { enabled -> + if (viewModel.isWriteSecureSettingsEnabled.value || viewModel.isShizukuPermissionGranted.value || viewModel.isRootPermissionGranted.value) { + viewModel.setAllowOverlaysInSettingsEnabled(enabled, context) + } else { + requestingPermissionFor = PermissionModule.ALLOW_OVERLAYS_IN_SETTINGS + } + }, + enabled = true, + onDisabledClick = { + if (!viewModel.isWriteSecureSettingsEnabled.value && !viewModel.isShizukuPermissionGranted.value && !viewModel.isRootPermissionGranted.value) { + requestingPermissionFor = PermissionModule.ALLOW_OVERLAYS_IN_SETTINGS + } + }, + iconRes = R.drawable.rounded_security_24, + modifier = Modifier.highlight(highlightSetting == "allow_overlays_in_settings_toggle") + ) + IconToggleItem( + title = stringResource(R.string.feat_transparent_navigation_bar_title), + description = stringResource(R.string.feat_transparent_navigation_bar_desc), + isChecked = viewModel.isTransparentNavigationBarEnabled.value, + onCheckedChange = { enabled -> + if (isShellGranted) { + viewModel.setTransparentNavigationBarEnabled(enabled, context) + } else { + requestingPermissionFor = PermissionModule.TRANSPARENT_NAVIGATION_BAR + } + }, + enabled = true, + onDisabledClick = { + if (!isShellGranted) { + requestingPermissionFor = PermissionModule.TRANSPARENT_NAVIGATION_BAR + } + }, + iconRes = R.drawable.rounded_bottom_navigation_24, + modifier = Modifier.highlight(highlightSetting == "transparent_navigation_bar_toggle") + ) AnimatedVisibility( visible = viewModel.isCircleToSearchGestureEnabled.value, @@ -274,5 +326,39 @@ fun OtherCustomizationsSettingsUI( ) } } + + Text( + text = stringResource(R.string.section_graphics), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 4.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + RoundedCardContainer( + modifier = Modifier, + spacing = 2.dp, + cornerRadius = 24.dp + ) { + IconToggleItem( + title = stringResource(R.string.feat_prefer_gpu_composing_title), + description = stringResource(R.string.feat_prefer_gpu_composing_desc), + isChecked = viewModel.isPreferGpuComposingEnabled.value, + onCheckedChange = { enabled -> + if (isShellGranted) { + viewModel.setPreferGpuComposingEnabled(enabled, context) + } else { + requestingPermissionFor = PermissionModule.PREFER_GPU_COMPOSING + } + }, + enabled = true, + onDisabledClick = { + if (!isShellGranted) { + requestingPermissionFor = PermissionModule.PREFER_GPU_COMPOSING + } + }, + iconRes = R.drawable.rounded_memory_alt_24, + modifier = Modifier.highlight(highlightSetting == "prefer_gpu_composing_toggle") + ) + } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/StandbyAppsSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/StandbyAppsSettingsUI.kt new file mode 100644 index 0000000000..4da4a9248a --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/StandbyAppsSettingsUI.kt @@ -0,0 +1,276 @@ +package com.sameerasw.essentials.ui.composables.configs + +import android.content.Intent +import android.net.Uri +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +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.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import coil.compose.AsyncImage +import com.sameerasw.essentials.R +import com.sameerasw.essentials.domain.model.AppStandbyInfo +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem +import com.sameerasw.essentials.ui.components.sheets.PermissionItem +import com.sameerasw.essentials.ui.components.sheets.PermissionsBottomSheet +import com.sameerasw.essentials.utils.HapticUtil +import com.sameerasw.essentials.viewmodels.MainViewModel + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun StandbyAppsSettingsUI( + viewModel: MainViewModel, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + var requestingPermissionFor by remember { mutableStateOf(false) } + + val isShizukuGranted = viewModel.isShizukuAvailable.value && viewModel.isShizukuPermissionGranted.value + val isRootGranted = viewModel.isRootAvailable.value && viewModel.isRootPermissionGranted.value + val isShellGranted = isShizukuGranted || isRootGranted + + LaunchedEffect(Unit) { + viewModel.loadStandbyApps(context) + } + + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + viewModel.loadStandbyApps(context) + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + + if (requestingPermissionFor) { + val shizukuPermission = PermissionItem( + iconRes = R.drawable.rounded_adb_24, + title = if (!viewModel.isShizukuAvailable.value) R.string.perm_shizuku_title else R.string.perm_shizuku_grant_title, + description = if (!viewModel.isShizukuAvailable.value) R.string.perm_shizuku_desc else R.string.perm_shizuku_grant_desc, + dependentFeatures = listOf(R.string.feat_standby_apps_title), + actionLabel = if (!viewModel.isShizukuAvailable.value) R.string.perm_shizuku_install_action else if (isShellGranted) R.string.perm_action_granted else R.string.perm_action_grant, + action = { + if (!viewModel.isShizukuAvailable.value) { + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=moe.shizuku.privileged.api") + ).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + context.startActivity(intent) + } else { + viewModel.requestShizukuPermission() + } + }, + isGranted = isShellGranted + ) + + PermissionsBottomSheet( + onDismissRequest = { requestingPermissionFor = false }, + featureTitle = R.string.feat_standby_apps_title, + permissions = listOf(shizukuPermission) + ) + } + + val appsList = viewModel.standbyAppsList.value + val isLoading = viewModel.isStandbyAppsLoading.value + + if (isLoading) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 32.dp), + horizontalArrangement = Arrangement.Center + ) { + LoadingIndicator() + } + } else { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + val buckets = listOf( + 10 to R.string.standby_bucket_active, + 20 to R.string.standby_bucket_working_set, + 30 to R.string.standby_bucket_frequent, + 40 to R.string.standby_bucket_rare, + 45 to R.string.standby_bucket_restricted + ) + + buckets.forEach { (bucketCode, titleRes) -> + val bucketApps = appsList.filter { it.bucket == bucketCode } + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = stringResource(titleRes) + " (${bucketApps.size})", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(start = 16.dp, bottom = 4.dp) + ) + + RoundedCardContainer{ + if (bucketApps.isEmpty()) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.standby_apps_empty_bucket), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + } else { + bucketApps.forEach { app -> + AppStandbyCardItem( + app = app, + currentBucket = bucketCode, + isShellGranted = isShellGranted, + onMoveBucket = { targetBucket -> + if (isShellGranted) { + viewModel.setAppStandbyBucket(app.packageName, targetBucket, context) + } else { + requestingPermissionFor = true + } + } + ) + } + } + } + } + } + } + } +} + +@Composable +private fun AppStandbyCardItem( + app: AppStandbyInfo, + currentBucket: Int, + isShellGranted: Boolean, + onMoveBucket: (Int) -> Unit +) { + val view = LocalView.current + var showMenu by remember { mutableStateOf(false) } + + ListItem( + content = { + Text( + text = app.label, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + supportingContent = { + Text( + text = app.packageName, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + leadingContent = { + AsyncImage( + model = app.icon, + contentDescription = app.label, + modifier = Modifier.size(24.dp) + ) + }, + trailingContent = { + Box { + IconButton( + onClick = { + HapticUtil.performUIHaptic(view) + if (isShellGranted) { + showMenu = true + } else { + onMoveBucket(currentBucket) + } + } + ) { + Icon( + painter = painterResource(R.drawable.rounded_mobiledata_arrows_24), + contentDescription = "Move bucket", + tint = MaterialTheme.colorScheme.primary + ) + } + + SegmentedDropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + val menuOptions = listOf( + 10 to R.string.standby_bucket_active, + 20 to R.string.standby_bucket_working_set, + 30 to R.string.standby_bucket_frequent, + 40 to R.string.standby_bucket_rare, + 45 to R.string.standby_bucket_restricted + ) + + menuOptions.forEach { (targetBucket, titleRes) -> + val isCurrent = targetBucket == currentBucket + SegmentedDropdownMenuItem( + text = { + Text(text = stringResource(titleRes)) + }, + enabled = !isCurrent, + onClick = { + HapticUtil.performUIHaptic(view) + showMenu = false + onMoveBucket(targetBucket) + } + ) + } + } + } + }, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + modifier = Modifier.fillMaxWidth() + ) +} diff --git a/app/src/main/java/com/sameerasw/essentials/utils/SurfaceFlingerControl.kt b/app/src/main/java/com/sameerasw/essentials/utils/SurfaceFlingerControl.kt new file mode 100644 index 0000000000..13694f2430 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/utils/SurfaceFlingerControl.kt @@ -0,0 +1,93 @@ +package com.sameerasw.essentials.utils + +import android.content.Context +import android.os.IBinder +import android.os.Parcel +import rikka.shizuku.ShizukuBinderWrapper +import rikka.shizuku.SystemServiceHelper + +object SurfaceFlingerControl { + + private const val SURFACE_COMPOSER_INTERFACE_KEY = "android.ui.ISurfaceComposer" + private const val SURFACE_FLINGER_DISABLE_OVERLAYS_CODE = 1008 + private const val SURFACE_FLINGER_READ_CODE = 1010 + + fun setDisableHwOverlays(disable: Boolean, isRoot: Boolean = false): Boolean { + val state = if (disable) 1 else 0 + + if (isRoot) { + return RootUtils.runCommand("service call SurfaceFlinger 1008 i32 $state") + } + + val rawBinder: IBinder? = try { + SystemServiceHelper.getSystemService("SurfaceFlinger") + } catch (e: Exception) { + null + } + + if (rawBinder != null) { + val binder = ShizukuBinderWrapper(rawBinder) + val data = Parcel.obtain() + return try { + data.writeInterfaceToken(SURFACE_COMPOSER_INTERFACE_KEY) + data.writeInt(state) + binder.transact( + SURFACE_FLINGER_DISABLE_OVERLAYS_CODE, + data, + null, + 0 + ) + true + } catch (e: Exception) { + ShizukuUtils.runCommand("service call SurfaceFlinger 1008 i32 $state") + true + } finally { + data.recycle() + } + } else { + ShizukuUtils.runCommand("service call SurfaceFlinger 1008 i32 $state") + return true + } + } + + fun isHwOverlaysDisabled(context: Context? = null, isRoot: Boolean = false): Boolean { + val rawBinder: IBinder? = try { + SystemServiceHelper.getSystemService("SurfaceFlinger") + } catch (e: Exception) { + null + } + + if (rawBinder != null) { + val binder = ShizukuBinderWrapper(rawBinder) + val data = Parcel.obtain() + val reply = Parcel.obtain() + try { + data.writeInterfaceToken(SURFACE_COMPOSER_INTERFACE_KEY) + binder.transact(SURFACE_FLINGER_READ_CODE, data, reply, 0) + reply.readInt() // showCpu + reply.readInt() // enableGL + reply.readInt() // showUpdates + reply.readInt() // showBackground + val disableOverlays = reply.readInt() // 5th integer + return disableOverlays != 0 + } catch (e: Exception) { + // Fallthrough to shell command output parsing + } finally { + data.recycle() + reply.recycle() + } + } + + if (context != null) { + val output = ShellUtils.runCommandWithOutput(context, "service call SurfaceFlinger 1010") + if (!output.isNullOrEmpty()) { + val hexValues = Regex("0x[0-9a-fA-F]+").findAll(output).map { it.value }.toList() + if (hexValues.size >= 5) { + return hexValues[4].substringAfter("0x").toIntOrNull(16) != 0 + } + } + } + + return false + } +} 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 b2769d1652..31f5348a2d 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -4,6 +4,8 @@ import android.Manifest import android.app.Activity import android.app.ActivityManager import android.app.admin.DevicePolicyManager +import android.app.usage.UsageStatsManager +import com.sameerasw.essentials.domain.model.AppStandbyInfo import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context @@ -58,6 +60,7 @@ import com.sameerasw.essentials.utils.AppUtil import com.sameerasw.essentials.utils.DeviceUtils import com.sameerasw.essentials.utils.PermissionUtils import com.sameerasw.essentials.utils.RefreshRateUtils +import com.sameerasw.essentials.utils.SurfaceFlingerControl import com.sameerasw.essentials.utils.RootUtils import com.sameerasw.essentials.utils.ShellUtils import com.sameerasw.essentials.utils.ShizukuUtils @@ -154,6 +157,14 @@ class MainViewModel : ViewModel() { val circleToSearchGestureHeight = mutableFloatStateOf(48f) val isCircleToSearchPreviewEnabled = mutableStateOf(false) val isDisableRotationSuggestionEnabled = mutableStateOf(false) + val isAllowOverlaysInSettingsEnabled = mutableStateOf(false) + val networkDownloadRateLimit = mutableIntStateOf(-1) + val isMobileDataAlwaysOnEnabled = mutableStateOf(false) + val isWirelessDisplayCertificationEnabled = mutableStateOf(false) + val isTransparentNavigationBarEnabled = mutableStateOf(false) + val isPreferGpuComposingEnabled = mutableStateOf(false) + val standbyAppsList = mutableStateOf>(emptyList()) + val isStandbyAppsLoading = mutableStateOf(false) val isPixelSearchbarEnabled = mutableStateOf(false) val pixelSearchbarType = mutableStateOf("empty") val pixelSearchbarDateFormat = mutableStateOf("EEEE, MMMM d") @@ -765,6 +776,72 @@ class MainViewModel : ViewModel() { } } + SettingsRepository.KEY_ALLOW_OVERLAYS_IN_SETTINGS -> { + isAllowOverlaysInSettingsEnabled.value = + settingsRepository.getBoolean(key) + appContext?.let { + applyAllowOverlaysInSettings( + it, + isAllowOverlaysInSettingsEnabled.value + ) + } + } + + SettingsRepository.KEY_NETWORK_DOWNLOAD_RATE_LIMIT -> { + networkDownloadRateLimit.intValue = + settingsRepository.getInt(key, -1) + appContext?.let { + applyNetworkDownloadRateLimit( + it, + networkDownloadRateLimit.intValue + ) + } + } + + SettingsRepository.KEY_MOBILE_DATA_ALWAYS_ON -> { + isMobileDataAlwaysOnEnabled.value = + settingsRepository.getBoolean(key) + appContext?.let { + applyMobileDataAlwaysOn( + it, + isMobileDataAlwaysOnEnabled.value + ) + } + } + + SettingsRepository.KEY_WIRELESS_DISPLAY_CERTIFICATION -> { + isWirelessDisplayCertificationEnabled.value = + settingsRepository.getBoolean(key) + appContext?.let { + applyWirelessDisplayCertification( + it, + isWirelessDisplayCertificationEnabled.value + ) + } + } + + SettingsRepository.KEY_TRANSPARENT_NAVIGATION_BAR -> { + isTransparentNavigationBarEnabled.value = + settingsRepository.getBoolean(key) + appContext?.let { + applyTransparentNavigationBar( + it, + isTransparentNavigationBarEnabled.value + ) + } + } + + SettingsRepository.KEY_PREFER_GPU_COMPOSING -> { + isPreferGpuComposingEnabled.value = + settingsRepository.getBoolean(key) + appContext?.let { + applyPreferGpuComposing( + it, + isPreferGpuComposingEnabled.value + ) + } + } + SettingsRepository.KEY_PIXEL_SEARCHBAR -> { isPixelSearchbarEnabled.value = settingsRepository.getBoolean(key) @@ -940,6 +1017,18 @@ class MainViewModel : ViewModel() { settingsRepository.getEdgeLightingSweepSelectedShapes() isDisableRotationSuggestionEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_DISABLE_ROTATION_SUGGESTION, false) + isAllowOverlaysInSettingsEnabled.value = + settingsRepository.getBoolean(SettingsRepository.KEY_ALLOW_OVERLAYS_IN_SETTINGS, false) + networkDownloadRateLimit.intValue = + settingsRepository.getInt(SettingsRepository.KEY_NETWORK_DOWNLOAD_RATE_LIMIT, -1) + isMobileDataAlwaysOnEnabled.value = + settingsRepository.getBoolean(SettingsRepository.KEY_MOBILE_DATA_ALWAYS_ON, false) + isWirelessDisplayCertificationEnabled.value = + settingsRepository.getBoolean(SettingsRepository.KEY_WIRELESS_DISPLAY_CERTIFICATION, false) + isTransparentNavigationBarEnabled.value = + settingsRepository.getBoolean(SettingsRepository.KEY_TRANSPARENT_NAVIGATION_BAR, false) + isPreferGpuComposingEnabled.value = + settingsRepository.getBoolean(SettingsRepository.KEY_PREFER_GPU_COMPOSING, false) isPixelSearchbarEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_PIXEL_SEARCHBAR, false) pixelSearchbarType.value = @@ -2130,6 +2219,255 @@ class MainViewModel : ViewModel() { applyDisableRotationSuggestion(context, enabled) } + fun setAllowOverlaysInSettingsEnabled(enabled: Boolean, context: Context) { + isAllowOverlaysInSettingsEnabled.value = enabled + settingsRepository.putBoolean(SettingsRepository.KEY_ALLOW_OVERLAYS_IN_SETTINGS, enabled) + applyAllowOverlaysInSettings(context, enabled) + } + + fun refreshAllowOverlaysInSettingsState(context: Context) { + try { + val currentVal = Settings.Secure.getInt(context.contentResolver, "secure_overlay_settings", 0) == 1 + isAllowOverlaysInSettingsEnabled.value = currentVal + settingsRepository.putBoolean(SettingsRepository.KEY_ALLOW_OVERLAYS_IN_SETTINGS, currentVal) + } catch (e: Exception) { + // Secure setting not accessible without permission + } + } + + private fun applyAllowOverlaysInSettings(context: Context, enabled: Boolean) { + val value = if (enabled) 1 else 0 + val key = "secure_overlay_settings" + var success = false + + if (PermissionUtils.canWriteSecureSettings(context)) { + try { + success = Settings.Secure.putInt(context.contentResolver, key, value) + } catch (e: Exception) { + e.printStackTrace() + } + } + + if (!success) { + viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) { + ShellUtils.runCommand(context, "settings put secure $key $value") + } + } + } + + fun setNetworkDownloadRateLimit(limit: Int, context: Context) { + networkDownloadRateLimit.intValue = limit + settingsRepository.putInt(SettingsRepository.KEY_NETWORK_DOWNLOAD_RATE_LIMIT, limit) + applyNetworkDownloadRateLimit(context, limit) + } + + fun setMobileDataAlwaysOnEnabled(enabled: Boolean, context: Context) { + isMobileDataAlwaysOnEnabled.value = enabled + settingsRepository.putBoolean(SettingsRepository.KEY_MOBILE_DATA_ALWAYS_ON, enabled) + applyMobileDataAlwaysOn(context, enabled) + } + + fun setWirelessDisplayCertificationEnabled(enabled: Boolean, context: Context) { + isWirelessDisplayCertificationEnabled.value = enabled + settingsRepository.putBoolean(SettingsRepository.KEY_WIRELESS_DISPLAY_CERTIFICATION, enabled) + applyWirelessDisplayCertification(context, enabled) + } + + fun refreshNetworksState(context: Context) { + try { + val liveRateLimit = Settings.Global.getInt(context.contentResolver, "ingress_rate_limit_bytes_per_second", -1) + networkDownloadRateLimit.intValue = liveRateLimit + settingsRepository.putInt(SettingsRepository.KEY_NETWORK_DOWNLOAD_RATE_LIMIT, liveRateLimit) + } catch (e: Exception) { + // Permission restricted + } + + try { + val liveMobileData = Settings.Global.getInt(context.contentResolver, "mobile_data_always_on", 0) == 1 + isMobileDataAlwaysOnEnabled.value = liveMobileData + settingsRepository.putBoolean(SettingsRepository.KEY_MOBILE_DATA_ALWAYS_ON, liveMobileData) + } catch (e: Exception) { + // Permission restricted + } + + try { + val liveWirelessDisplay = Settings.Global.getInt(context.contentResolver, "wifi_display_certification_on", 0) == 1 + isWirelessDisplayCertificationEnabled.value = liveWirelessDisplay + settingsRepository.putBoolean(SettingsRepository.KEY_WIRELESS_DISPLAY_CERTIFICATION, liveWirelessDisplay) + } catch (e: Exception) { + // Permission restricted + } + } + + private fun applyNetworkDownloadRateLimit(context: Context, limit: Int) { + val key = "ingress_rate_limit_bytes_per_second" + var success = false + if (PermissionUtils.canWriteSecureSettings(context)) { + try { + success = Settings.Global.putInt(context.contentResolver, key, limit) + } catch (e: Exception) { + e.printStackTrace() + } + } + if (!success) { + viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) { + ShellUtils.runCommand(context, "settings put global $key $limit") + } + } + } + + private fun applyMobileDataAlwaysOn(context: Context, enabled: Boolean) { + val value = if (enabled) 1 else 0 + val key = "mobile_data_always_on" + var success = false + if (PermissionUtils.canWriteSecureSettings(context)) { + try { + success = Settings.Global.putInt(context.contentResolver, key, value) + } catch (e: Exception) { + e.printStackTrace() + } + } + if (!success) { + viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) { + ShellUtils.runCommand(context, "settings put global $key $value") + } + } + } + + private fun applyWirelessDisplayCertification(context: Context, enabled: Boolean) { + val value = if (enabled) 1 else 0 + val key = "wifi_display_certification_on" + var success = false + if (PermissionUtils.canWriteSecureSettings(context)) { + try { + success = Settings.Global.putInt(context.contentResolver, key, value) + } catch (e: Exception) { + e.printStackTrace() + } + } + if (!success) { + viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) { + ShellUtils.runCommand(context, "settings put global $key $value") + } + } + } + + fun setTransparentNavigationBarEnabled(enabled: Boolean, context: Context) { + isTransparentNavigationBarEnabled.value = enabled + settingsRepository.putBoolean(SettingsRepository.KEY_TRANSPARENT_NAVIGATION_BAR, enabled) + applyTransparentNavigationBar(context, enabled) + } + + fun refreshTransparentNavigationBarState(context: Context) { + viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) { + val pkg = "com.android.internal.systemui.navbar.transparent" + val output = ShellUtils.runCommandWithOutput(context, "cmd overlay list") + if (output != null) { + val isEnabled = output.lines().any { line -> + line.contains("[x]") && line.contains(pkg) + } + kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) { + isTransparentNavigationBarEnabled.value = isEnabled + settingsRepository.putBoolean(SettingsRepository.KEY_TRANSPARENT_NAVIGATION_BAR, isEnabled) + } + } + } + } + + private fun applyTransparentNavigationBar(context: Context, enabled: Boolean) { + val pkg = "com.android.internal.systemui.navbar.transparent" + val action = if (enabled) "enable" else "disable" + viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) { + ShellUtils.runCommand(context, "cmd overlay $action --user current $pkg") + } + } + + fun loadStandbyApps(context: Context) { + viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) { + isStandbyAppsLoading.value = true + val pm = context.packageManager + val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) } + val apps = pm.queryIntentActivities(intent, 0) + + val appList = apps.mapNotNull { resolveInfo -> + val pkg = resolveInfo.activityInfo.packageName + if (pkg == context.packageName) return@mapNotNull null + + val label = resolveInfo.loadLabel(pm).toString() + val icon = resolveInfo.loadIcon(pm) + + var bucket = 10 + val output = ShellUtils.runCommandWithOutput(context, "am get-standby-bucket $pkg") + if (output != null) { + val text = output.lowercase().trim() + bucket = when { + text.contains("restricted") || text.contains("45") -> 45 + text.contains("rare") || text.contains("40") -> 40 + text.contains("frequent") || text.contains("30") -> 30 + text.contains("working") || text.contains("20") -> 20 + text.contains("active") || text.contains("10") -> 10 + else -> 10 + } + } + + AppStandbyInfo(pkg, label, icon, bucket) + }.sortedBy { it.label.lowercase() } + + kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) { + standbyAppsList.value = appList + isStandbyAppsLoading.value = false + } + } + } + + fun setAppStandbyBucket(packageName: String, targetBucket: Int, context: Context) { + val currentList = standbyAppsList.value + val updatedList = currentList.map { app -> + if (app.packageName == packageName) { + app.copy(bucket = targetBucket) + } else app + } + standbyAppsList.value = updatedList + + viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) { + val bucketName = when (targetBucket) { + 10 -> "active" + 20 -> "working_set" + 30 -> "frequent" + 40 -> "rare" + 45 -> "restricted" + else -> "active" + } + ShellUtils.runCommand(context, "am set-standby-bucket $packageName $bucketName") + } + } + + fun setPreferGpuComposingEnabled(enabled: Boolean, context: Context) { + isPreferGpuComposingEnabled.value = enabled + settingsRepository.putBoolean(SettingsRepository.KEY_PREFER_GPU_COMPOSING, enabled) + applyPreferGpuComposing(context, enabled) + } + + fun refreshPreferGpuComposingState(context: Context) { + viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) { + val isShellGranted = (isShizukuAvailable.value && isShizukuPermissionGranted.value) || + (isRootAvailable.value && isRootPermissionGranted.value) + if (isShellGranted) { + val isRoot = isRootAvailable.value && isRootPermissionGranted.value + val liveValue = SurfaceFlingerControl.isHwOverlaysDisabled(context, isRoot) + isPreferGpuComposingEnabled.value = liveValue + settingsRepository.putBoolean(SettingsRepository.KEY_PREFER_GPU_COMPOSING, liveValue) + } + } + } + + private fun applyPreferGpuComposing(context: Context, enabled: Boolean) { + viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) { + val isRoot = isRootAvailable.value && isRootPermissionGranted.value + SurfaceFlingerControl.setDisableHwOverlays(enabled, isRoot) + } + } + private fun applyDisableRotationSuggestion(context: Context, enabled: Boolean) { val value = if (enabled) 0 else 1 val key = "show_rotation_suggestions" diff --git a/app/src/main/res/drawable/rounded_bottom_navigation_24.xml b/app/src/main/res/drawable/rounded_bottom_navigation_24.xml new file mode 100644 index 0000000000..ce0a9ab870 --- /dev/null +++ b/app/src/main/res/drawable/rounded_bottom_navigation_24.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/rounded_mobiledata_arrows_24.xml b/app/src/main/res/drawable/rounded_mobiledata_arrows_24.xml new file mode 100644 index 0000000000..8ba846166b --- /dev/null +++ b/app/src/main/res/drawable/rounded_mobiledata_arrows_24.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 541167f31b..b8c2257129 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -442,9 +442,9 @@ Mode Always-on Afficher l\'heure et des infos quand l\'écran est éteint Synchronisation des calendriers - Sync your phone calendar to your watch - Lock from Watch - Lock your phone remotely + Synchronise ton calendrier de téléphone sur ta montre + Verrouillage depuis la montre + Verrouille ton téléphone à distance Lock Mode Screen off Lock @@ -1413,8 +1413,8 @@ Freeze now Abort - Lock screen clock - Customize lock screen clock on Pixels + Horloge d\'écran de verrouillage + Customise l\'horloge d\'écran de verrouillage sur les Pixels Change the lock screen clock style on Pixels. Select from the available clock faces provided by the System UI. Some old styles might not be available. Big Number Calligraphy @@ -1465,4 +1465,11 @@ Remplacer la barre de recherche Remplacer la barre de recherche par défaut de Pixel Launcher. Mode poche + Prévient les contacts accidentels dans la poche + Customise + Customise le contrôle des applications WearOS + Mode de son synchronisé + Synchronise automatiquement avec la montre + Débogage sans fil + Active à distance diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 47a6d73f46..e648aac441 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1805,6 +1805,35 @@ Shortcut created for %s Disable rotation suggestion Do not show the rotation button when auto-rotate is off + Allow overlays in Settings + Allow apps to draw overlays over system Settings + Networks + Networks + Control radios and networks + Network download rate limit + Limit system-wide download speed + Disabled + Mobile data always on + Keep mobile data active even when Wi-Fi is connected + Wireless display certification + Show options for wireless display certification + Transparent navigation bar + Make system navigation bar background transparent + Standby apps + Optimize app background activities + Configure network download speed limits, keep mobile data continuously active even when connected to Wi-Fi, and enable wireless display certification settings. + Manage Android App Standby Buckets (Active, Working set, Frequent, Rare, Restricted) to control background app execution, network usage, and battery consumption. + Advanced display and system UI customizations including gesture bar visibility, circle to search height, rotation suggestions, setting overlays, transparent navigation bar, and GPU screen compositing. + Active + Working set + Frequent + Rare + Restricted + No apps in this bucket + Loading standby apps... + Graphics + Prefer GPU for screen composing + Disable hardware overlays to force GPU screen compositing. Resets on device reboot. Replace searchbar Replace Pixel Launcher default searchbar. App got shut up