diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c46fab63a..af8639f2e 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -39,8 +39,8 @@ android {
applicationId = "com.sameerasw.essentials"
minSdk = 26
targetSdk = 37
- versionCode = 57
- versionName = "16.5"
+ versionCode = 58
+ versionName = "16.6"
val whatsNewCounter = 2
buildConfigField("int", "WHATS_NEW_COUNTER", whatsNewCounter.toString())
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 43acf0d19..970dba07d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -534,6 +534,36 @@
android:resource="@xml/favorites_widget_info" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt b/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt
index 1c0cbb82e..f1e2e19d5 100644
--- a/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt
@@ -161,6 +161,10 @@ class ShutUpShortcutActivity : ComponentActivity() {
)
}
+ if (config.disableUsbDebugging || config.disableWirelessDebugging) {
+ com.sameerasw.essentials.utils.ShizukuUtils.stopShizuku(this@ShutUpShortcutActivity)
+ }
+
// Always explicitly disable USB debugging if requested, even if dev options were already disabled
// as some apps check this specific setting directly.
if (config.disableUsbDebugging) {
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 463642d31..f086c578b 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
@@ -188,6 +188,7 @@ class SettingsRepository(private val context: Context) {
const val KEY_BATTERY_WIDGET_BACKGROUND_ENABLED = "battery_widget_background_enabled"
const val KEY_PINNED_FEATURES = "pinned_features"
+ const val KEY_PINNED_QS_TILES = "pinned_qs_tiles"
const val KEY_LIKE_SONG_TOAST_ENABLED = "like_song_toast_enabled"
const val KEY_LIKE_SONG_AOD_OVERLAY_ENABLED = "like_song_aod_overlay_enabled"
const val KEY_AMBIENT_MUSIC_GLANCE_ENABLED = "ambient_music_glance_enabled"
@@ -741,7 +742,8 @@ class SettingsRepository(private val context: Context) {
if (key == KEY_GITHUB_ACCESS_TOKEN || key == KEY_GITHUB_WORKFLOW_TOKEN ||
key == KEY_SHIZUKU_AUTH_TOKEN || key.startsWith("mac_battery_") ||
key == "airsync_mac_connected" || key == KEY_SNOOZE_DISCOVERED_CHANNELS ||
- key == KEY_MAPS_DISCOVERED_CHANNELS || key == KEY_SHUT_UP_ORIGINAL_SETTINGS
+ key == KEY_MAPS_DISCOVERED_CHANNELS || key == KEY_SHUT_UP_ORIGINAL_SETTINGS ||
+ key == "battery_history_points"
) {
return@forEach
}
@@ -914,6 +916,22 @@ class SettingsRepository(private val context: Context) {
putString(KEY_PINNED_FEATURES, json)
}
+ fun getPinnedQsTiles(): List {
+ val json = prefs.getString(KEY_PINNED_QS_TILES, null)
+ return if (json != null) {
+ try {
+ gson.fromJson(json, Array::class.java).toList()
+ } catch (e: Exception) {
+ emptyList()
+ }
+ } else emptyList()
+ }
+
+ fun savePinnedQsTiles(tiles: List) {
+ val json = gson.toJson(tiles)
+ putString(KEY_PINNED_QS_TILES, json)
+ }
+
fun getRecentSearches(): List {
val json = prefs.getString(KEY_RECENT_SEARCHES, null)
return if (json != null) {
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/StatusBarIcon.kt b/app/src/main/java/com/sameerasw/essentials/domain/StatusBarIcon.kt
index d7ed8ab01..348b8942a 100644
--- a/app/src/main/java/com/sameerasw/essentials/domain/StatusBarIcon.kt
+++ b/app/src/main/java/com/sameerasw/essentials/domain/StatusBarIcon.kt
@@ -155,7 +155,7 @@ object StatusBarIconRegistry {
StatusBarIcon(
id = "headset",
displayNameRes = R.string.icon_headset,
- blacklistNames = listOf("headset", "earphone"),
+ blacklistNames = listOf("headset", "earphone", "headset_system"),
defaultVisible = false,
categoryRes = CAT_AUDIO_MEDIA,
iconRes = R.drawable.rounded_headset_mic_24
@@ -179,7 +179,7 @@ object StatusBarIconRegistry {
StatusBarIcon(
id = "clock",
displayNameRes = R.string.icon_clock,
- blacklistNames = listOf("clock"),
+ blacklistNames = listOf("clock", "clock_meridiem"),
categoryRes = CAT_SYSTEM_STATUS,
iconRes = R.drawable.rounded_nest_clock_farsight_analog_24
),
@@ -269,6 +269,13 @@ object StatusBarIconRegistry {
categoryRes = CAT_SYSTEM_STATUS,
iconRes = R.drawable.rounded_security_24
),
+ StatusBarIcon(
+ id = "network_speed",
+ displayNameRes = R.string.icon_network_speed,
+ blacklistNames = listOf("network_speed"),
+ categoryRes = CAT_SYSTEM_STATUS,
+ iconRes = null
+ ),
// --- OEM Specific ---
StatusBarIcon(
@@ -282,7 +289,6 @@ object StatusBarIconRegistry {
id = "samsung_smart",
displayNameRes = R.string.icon_samsung_smart,
blacklistNames = listOf(
- "glove",
"gesture",
"smart_scroll",
"face",
@@ -292,6 +298,27 @@ object StatusBarIconRegistry {
categoryRes = CAT_OEM_SPECIFIC,
iconRes = R.drawable.rounded_fiber_smart_record_24
),
+ StatusBarIcon(
+ id = "glove_mode",
+ displayNameRes = R.string.icon_glove_mode,
+ blacklistNames = listOf("glove_mode", "glove"),
+ categoryRes = CAT_OEM_SPECIFIC,
+ iconRes = null
+ ),
+ StatusBarIcon(
+ id = "frame_rate",
+ displayNameRes = R.string.icon_frame_rate,
+ blacklistNames = listOf("frame_rate_mode"),
+ categoryRes = CAT_OEM_SPECIFIC,
+ iconRes = null
+ ),
+ StatusBarIcon(
+ id = "no_sim",
+ displayNameRes = R.string.icon_no_sim,
+ blacklistNames = listOf("nosim"),
+ categoryRes = CAT_PHONE_NETWORK,
+ iconRes = null
+ ),
StatusBarIcon(
id = "samsung_services",
displayNameRes = R.string.icon_samsung_services,
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 1161d4869..670734ce8 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
@@ -271,6 +271,8 @@ object FeatureRegistry {
) {
override fun isEnabled(viewModel: MainViewModel) = true
override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {}
+
+ override fun isDeviceSupported(context: Context) = DeviceUtils.isGoogleDevice()
},
object : Feature(
id = "Lock screen clock",
@@ -1338,7 +1340,7 @@ object FeatureRegistry {
context.startActivity(
Intent(
context,
- com.sameerasw.essentials.AppUpdatesActivity::class.java
+ com.sameerasw.essentials.ui.activities.YourAndroidActivity::class.java
)
)
}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt
index 028981a57..8de3a1a27 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/DeviceInfoSyncManager.kt
@@ -41,6 +41,11 @@ object DeviceInfoSyncManager {
maxTorchLevel = FlashlightUtil.getMaxLevel(context, cameraId)
isIntensitySupported = FlashlightUtil.isIntensitySupported(context, cameraId)
syncDeviceInfo(context)
+
+ val intent = Intent("com.sameerasw.essentials.action.QS_TILES_WIDGET_UPDATE").apply {
+ setPackage(context.packageName)
+ }
+ context.sendBroadcast(intent)
}
}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt
index 8fe150c45..a9cc210b5 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt
@@ -141,6 +141,7 @@ class FlashlightHandler(
} else {
getCameraId()?.let { id ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
+ !com.sameerasw.essentials.utils.DeviceUtils.isMediatekDevice() &&
FlashlightUtil.isIntensitySupported(service, id)
) {
try {
diff --git a/app/src/main/java/com/sameerasw/essentials/services/receivers/BatteryAutomationReceiver.kt b/app/src/main/java/com/sameerasw/essentials/services/receivers/BatteryAutomationReceiver.kt
new file mode 100644
index 000000000..aa28c694d
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/receivers/BatteryAutomationReceiver.kt
@@ -0,0 +1,39 @@
+package com.sameerasw.essentials.services.receivers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.BatteryManager
+import com.sameerasw.essentials.data.repository.SettingsRepository
+import com.sameerasw.essentials.utils.BatteryStatsUtil
+
+class BatteryAutomationReceiver : BroadcastReceiver() {
+
+ override fun onReceive(context: Context, intent: Intent) {
+ val action = intent.action ?: return
+ val bm = context.getSystemService(Context.BATTERY_SERVICE) as? BatteryManager
+ val level = bm?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) ?: -1
+ val isPlugged = action == Intent.ACTION_POWER_CONNECTED || (action == Intent.ACTION_BATTERY_CHANGED && intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) > 0)
+
+ if (level >= 0) {
+ val force = action == Intent.ACTION_POWER_CONNECTED || action == Intent.ACTION_POWER_DISCONNECTED
+ com.sameerasw.essentials.utils.BatteryHistoryManager.recordPoint(context, level, isPlugged, forceRecord = force)
+ }
+
+ if (action == Intent.ACTION_POWER_DISCONNECTED) {
+ checkAutoResetBatteryStats(context)
+ }
+ }
+
+ private fun checkAutoResetBatteryStats(context: Context) {
+ val repo = SettingsRepository(context)
+ val autoResetEnabled = repo.getBoolean("auto_reset_battery_stats", false)
+ if (autoResetEnabled) {
+ val bm = context.getSystemService(Context.BATTERY_SERVICE) as? BatteryManager
+ val level = bm?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) ?: -1
+ if (level >= 90) {
+ BatteryStatsUtil.resetStats(context)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/receivers/QsTileActionRouter.kt b/app/src/main/java/com/sameerasw/essentials/services/receivers/QsTileActionRouter.kt
new file mode 100644
index 000000000..b47141168
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/receivers/QsTileActionRouter.kt
@@ -0,0 +1,83 @@
+package com.sameerasw.essentials.services.receivers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import com.sameerasw.essentials.MainActivity
+import com.sameerasw.essentials.domain.controller.CaffeinateController
+import com.sameerasw.essentials.services.tiles.*
+import com.sameerasw.essentials.utils.HapticUtil
+
+class QsTileActionRouter : BroadcastReceiver() {
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action != ACTION_TRIGGER_TILE) return
+
+ val serviceClassName = intent.getStringExtra(EXTRA_SERVICE_CLASS_NAME) ?: return
+ Log.d("QsTileActionRouter", "Triggering tile action for: $serviceClassName")
+
+ // Perform haptics on click trigger
+ HapticUtil.performHapticForService(context)
+
+ try {
+ when (serviceClassName) {
+ CaffeinateTileService::class.java.name -> {
+ if (CaffeinateController.isStarting.value) {
+ CaffeinateController.cycleTimeout(context)
+ } else {
+ CaffeinateController.toggle(context)
+ }
+ }
+ FlashlightTileService::class.java.name -> {
+ val flashlightIntent = Intent(context, FlashlightActionReceiver::class.java).apply {
+ action = FlashlightActionReceiver.ACTION_TOGGLE
+ }
+ context.sendBroadcast(flashlightIntent)
+ }
+ else -> {
+ val clazz = Class.forName(serviceClassName)
+ if (BaseTileService::class.java.isAssignableFrom(clazz)) {
+ val tileService = clazz.getDeclaredConstructor().newInstance() as BaseTileService
+
+ val attachBaseContextMethod = android.content.ContextWrapper::class.java.getDeclaredMethod(
+ "attachBaseContext",
+ Context::class.java
+ )
+ attachBaseContextMethod.isAccessible = true
+ attachBaseContextMethod.invoke(tileService, context)
+
+ tileService.onTileClick()
+ } else {
+ fallbackLaunch(context)
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.e("QsTileActionRouter", "Failed to trigger tile $serviceClassName headlessly", e)
+ fallbackLaunch(context)
+ }
+
+ // Notify widget to update state after action
+ val updateIntent = Intent("com.sameerasw.essentials.action.QS_TILES_WIDGET_UPDATE").apply {
+ setPackage(context.packageName)
+ }
+ context.sendBroadcast(updateIntent)
+ }
+
+ private fun fallbackLaunch(context: Context) {
+ try {
+ val launchIntent = Intent(context, MainActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ context.startActivity(launchIntent)
+ } catch (e: Exception) {
+ Log.e("QsTileActionRouter", "Failed to launch activity", e)
+ }
+ }
+
+ companion object {
+ const val ACTION_TRIGGER_TILE = "com.sameerasw.essentials.action.TRIGGER_QS_TILE"
+ const val EXTRA_SERVICE_CLASS_NAME = "extra_service_class_name"
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/ColorPickerTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/ColorPickerTileService.kt
index 922821dd4..569e21411 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/tiles/ColorPickerTileService.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/ColorPickerTileService.kt
@@ -42,18 +42,30 @@ class ColorPickerTileService : BaseTileService() {
intent,
PendingIntent.FLAG_IMMUTABLE
)
- startActivityAndCollapse(pendingIntent)
+ try {
+ startActivityAndCollapse(pendingIntent)
+ } catch (e: Exception) {
+ startActivity(intent)
+ }
} else {
- @Suppress("DEPRECATION")
- startActivityAndCollapse(intent)
+ try {
+ @Suppress("DEPRECATION")
+ startActivityAndCollapse(intent)
+ } catch (e: Exception) {
+ startActivity(intent)
+ }
}
} catch (e: Exception) {
- Handler(Looper.getMainLooper()).post {
- Toast.makeText(
- applicationContext,
- getString(R.string.toast_eyedropper_failed),
- Toast.LENGTH_SHORT
- ).show()
+ try {
+ startActivity(intent)
+ } catch (ex: Exception) {
+ Handler(Looper.getMainLooper()).post {
+ Toast.makeText(
+ applicationContext,
+ getString(R.string.toast_eyedropper_failed),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
}
}
}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/FlashlightTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/FlashlightTileService.kt
index 5d4e8112b..eaa9e8841 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/tiles/FlashlightTileService.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/FlashlightTileService.kt
@@ -19,6 +19,11 @@ class FlashlightTileService : BaseTileService() {
super.onTorchModeChanged(cameraId, enabled)
isTorchOn = enabled
updateTile()
+
+ val intent = Intent("com.sameerasw.essentials.action.QS_TILES_WIDGET_UPDATE").apply {
+ setPackage(packageName)
+ }
+ sendBroadcast(intent)
}
}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/QsTileRegistry.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/QsTileRegistry.kt
new file mode 100644
index 000000000..ea50e6e86
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/QsTileRegistry.kt
@@ -0,0 +1,174 @@
+package com.sameerasw.essentials.services.tiles
+
+import com.sameerasw.essentials.R
+
+object QsTileRegistry {
+ data class QsTileEntry(
+ val titleRes: Int,
+ val iconRes: Int,
+ val serviceClass: Class<*>
+ )
+
+ val ALL_TILES: List = listOf(
+ QsTileEntry(R.string.tile_ui_blur, R.drawable.rounded_blur_on_24, UiBlurTileService::class.java),
+ QsTileEntry(R.string.tile_bubbles, R.drawable.rounded_bubble_24, BubblesTileService::class.java),
+ QsTileEntry(R.string.tile_sensitive_content, R.drawable.rounded_notifications_off_24, PrivateNotificationsTileService::class.java),
+ QsTileEntry(R.string.tile_tap_to_wake, R.drawable.rounded_touch_app_24, TapToWakeTileService::class.java),
+ QsTileEntry(R.string.tile_aod, R.drawable.rounded_mobile_text_2_24, AlwaysOnDisplayTileService::class.java),
+ QsTileEntry(R.string.tile_caffeinate, R.drawable.rounded_coffee_24, CaffeinateTileService::class.java),
+ QsTileEntry(R.string.tile_sound_mode, R.drawable.rounded_volume_up_24, SoundModeTileService::class.java),
+ QsTileEntry(R.string.tile_notification_lighting, R.drawable.rounded_blur_linear_24, NotificationLightingTileService::class.java),
+ QsTileEntry(R.string.tile_dynamic_night_light, R.drawable.rounded_nightlight_24, DynamicNightLightTileService::class.java),
+ QsTileEntry(R.string.tile_locked_security, R.drawable.rounded_security_24, ScreenLockedSecurityTileService::class.java),
+ QsTileEntry(R.string.tile_app_lock, R.drawable.rounded_shield_lock_24, AppLockTileService::class.java),
+ QsTileEntry(R.string.tile_mono_audio, R.drawable.rounded_headphones_24, MonoAudioTileService::class.java),
+ QsTileEntry(R.string.tile_flashlight, R.drawable.rounded_flashlight_on_24, FlashlightTileService::class.java),
+ QsTileEntry(R.string.tile_app_freezing, R.drawable.rounded_app_badging_24, AppFreezingTileService::class.java),
+ QsTileEntry(R.string.tile_flashlight_pulse, R.drawable.outline_backlight_high_24, FlashlightPulseTileService::class.java),
+ QsTileEntry(R.string.tile_stay_awake, R.drawable.rounded_av_timer_24, StayAwakeTileService::class.java),
+ QsTileEntry(R.string.nfc_tile_label, R.drawable.rounded_nfc_24, NfcTileService::class.java),
+ QsTileEntry(R.string.tile_adaptive_brightness, R.drawable.rounded_brightness_auto_24, AdaptiveBrightnessTileService::class.java),
+ QsTileEntry(R.string.tile_scale_animations, R.drawable.rounded_front_hand_24, ScaleAnimationsTileService::class.java),
+ QsTileEntry(R.string.tile_restart_systemui, R.drawable.reopen_window_24px, RestartSystemUiTileService::class.java),
+ QsTileEntry(R.string.tile_refresh_rate, R.drawable.rounded_shutter_speed_24, RefreshRateTileService::class.java),
+ QsTileEntry(R.string.feat_maps_power_saving_title, R.drawable.rounded_navigation_24, MapsPowerSavingTileService::class.java),
+ QsTileEntry(R.string.tile_private_dns, R.drawable.rounded_dns_24, PrivateDnsTileService::class.java),
+ QsTileEntry(R.string.tile_usb_debugging, R.drawable.rounded_adb_24, UsbDebuggingTileService::class.java),
+ QsTileEntry(R.string.tile_color_picker, R.drawable.rounded_colorize_24, ColorPickerTileService::class.java),
+ QsTileEntry(R.string.tile_developer_options, R.drawable.rounded_mobile_code_24, DeveloperOptionsTileService::class.java),
+ QsTileEntry(R.string.tile_charge_optimization, R.drawable.rounded_battery_android_frame_shield_24, ChargeQuickTileService::class.java)
+ )
+
+ private val tilesMapByClassName: Map by lazy {
+ ALL_TILES.associateBy { it.serviceClass.name }
+ }
+
+ fun getTileByClassName(className: String): QsTileEntry? {
+ return tilesMapByClassName[className]
+ }
+
+ fun isTileActive(context: android.content.Context, className: String): Boolean {
+ return try {
+ when (className) {
+ CaffeinateTileService::class.java.name -> {
+ com.sameerasw.essentials.domain.controller.CaffeinateController.isActive.value ||
+ com.sameerasw.essentials.domain.controller.CaffeinateController.isStarting.value
+ }
+ FlashlightTileService::class.java.name -> {
+ val instance = ScreenOffAccessibilityService.instance
+ if (instance != null) {
+ instance.flashlightHandler.isTorchOn
+ } else {
+ val clazz = Class.forName(className)
+ val tileService = clazz.getDeclaredConstructor().newInstance() as BaseTileService
+ val attachBaseContextMethod = android.content.ContextWrapper::class.java.getDeclaredMethod(
+ "attachBaseContext",
+ android.content.Context::class.java
+ )
+ attachBaseContextMethod.isAccessible = true
+ attachBaseContextMethod.invoke(tileService, context)
+ val getTileStateMethod = BaseTileService::class.java.getDeclaredMethod("getTileState")
+ getTileStateMethod.isAccessible = true
+ (getTileStateMethod.invoke(tileService) as Int) == android.service.quicksettings.Tile.STATE_ACTIVE
+ }
+ }
+ else -> {
+ val clazz = Class.forName(className)
+ if (BaseTileService::class.java.isAssignableFrom(clazz)) {
+ val tileService = clazz.getDeclaredConstructor().newInstance() as BaseTileService
+ val attachBaseContextMethod = android.content.ContextWrapper::class.java.getDeclaredMethod(
+ "attachBaseContext",
+ android.content.Context::class.java
+ )
+ attachBaseContextMethod.isAccessible = true
+ attachBaseContextMethod.invoke(tileService, context)
+
+ val getTileStateMethod = BaseTileService::class.java.getDeclaredMethod("getTileState")
+ getTileStateMethod.isAccessible = true
+ val state = getTileStateMethod.invoke(tileService) as Int
+ state == android.service.quicksettings.Tile.STATE_ACTIVE
+ } else false
+ }
+ }
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ fun getTileSubtitle(context: android.content.Context, className: String): String {
+ return try {
+ when (className) {
+ CaffeinateTileService::class.java.name -> {
+ if (com.sameerasw.essentials.domain.controller.CaffeinateController.isActive.value) {
+ "Active"
+ } else if (com.sameerasw.essentials.domain.controller.CaffeinateController.isStarting.value) {
+ "Starting"
+ } else "Off"
+ }
+ FlashlightTileService::class.java.name -> {
+ val instance = ScreenOffAccessibilityService.instance
+ val isTorchOn = if (instance != null) {
+ instance.flashlightHandler.isTorchOn
+ } else false
+ if (isTorchOn) "On" else "Off"
+ }
+ else -> {
+ val clazz = Class.forName(className)
+ if (BaseTileService::class.java.isAssignableFrom(clazz)) {
+ val tileService = clazz.getDeclaredConstructor().newInstance() as BaseTileService
+ val attachBaseContextMethod = android.content.ContextWrapper::class.java.getDeclaredMethod(
+ "attachBaseContext",
+ android.content.Context::class.java
+ )
+ attachBaseContextMethod.isAccessible = true
+ attachBaseContextMethod.invoke(tileService, context)
+
+ val getTileSubtitleMethod = BaseTileService::class.java.getDeclaredMethod("getTileSubtitle")
+ getTileSubtitleMethod.isAccessible = true
+ (getTileSubtitleMethod.invoke(tileService) as? String) ?: ""
+ } else ""
+ }
+ }
+ } catch (e: Exception) {
+ ""
+ }
+ }
+
+ fun getTileIcon(context: android.content.Context, className: String, fallbackIconRes: Int): Int {
+ return try {
+ when (className) {
+ FlashlightTileService::class.java.name -> {
+ val instance = ScreenOffAccessibilityService.instance
+ val isTorchOn = if (instance != null) {
+ instance.flashlightHandler.isTorchOn
+ } else false
+ if (isTorchOn) R.drawable.round_flashlight_on_24 else R.drawable.rounded_flashlight_on_24
+ }
+ else -> {
+ val clazz = Class.forName(className)
+ if (BaseTileService::class.java.isAssignableFrom(clazz)) {
+ val tileService = clazz.getDeclaredConstructor().newInstance() as BaseTileService
+ val attachBaseContextMethod = android.content.ContextWrapper::class.java.getDeclaredMethod(
+ "attachBaseContext",
+ android.content.Context::class.java
+ )
+ attachBaseContextMethod.isAccessible = true
+ attachBaseContextMethod.invoke(tileService, context)
+
+ val getTileIconMethod = BaseTileService::class.java.getDeclaredMethod("getTileIcon")
+ getTileIconMethod.isAccessible = true
+ val iconObj = getTileIconMethod.invoke(tileService) as? android.graphics.drawable.Icon
+ if (iconObj != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
+ val resId = iconObj.resId
+ if (resId != 0) resId else fallbackIconRes
+ } else {
+ fallbackIconRes
+ }
+ } else fallbackIconRes
+ }
+ }
+ } catch (e: Exception) {
+ fallbackIconRes
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/RefreshRateTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/RefreshRateTileService.kt
index 1e8b06124..9d3f5e62b 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/tiles/RefreshRateTileService.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/RefreshRateTileService.kt
@@ -8,6 +8,7 @@ import androidx.annotation.RequiresApi
import com.sameerasw.essentials.FeatureSettingsActivity
import com.sameerasw.essentials.R
import com.sameerasw.essentials.data.repository.SettingsRepository
+import com.sameerasw.essentials.utils.DeviceUtils
import com.sameerasw.essentials.utils.RefreshRateUtils
import com.sameerasw.essentials.utils.ShellUtils
@@ -54,6 +55,8 @@ class RefreshRateTileService : BaseTileService() {
override fun getTileSubtitle(): String = RefreshRateUtils.getDisplaySubtitle(this)
+ override fun isDeviceSupported(): Boolean = DeviceUtils.isGoogleDevice()
+
override fun hasFeaturePermission(): Boolean = ShellUtils.hasPermission(this)
override fun getTileIcon(): Icon {
diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt
index 26d8eaf1d..4381a2198 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt
@@ -47,7 +47,7 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene
private var proximitySensor: Sensor? = null
// Handlers
- private lateinit var flashlightHandler: FlashlightHandler
+ lateinit var flashlightHandler: FlashlightHandler
private lateinit var notificationLightingHandler: NotificationLightingHandler
private lateinit var buttonRemapHandler: ButtonRemapHandler
private lateinit var appFlowHandler: AppFlowHandler
diff --git a/app/src/main/java/com/sameerasw/essentials/services/widgets/FavoritesWidget.kt b/app/src/main/java/com/sameerasw/essentials/services/widgets/FavoritesWidget.kt
index 751b58d19..0df6d946e 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/widgets/FavoritesWidget.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/widgets/FavoritesWidget.kt
@@ -100,9 +100,15 @@ class FavoritesWidget : GlanceAppWidget() {
val cellWidth = width / columnsCount.toFloat()
val cellHeight = height / rowsCount.toFloat()
+ val scrollMode = if (Build.VERSION.SDK_INT >= 37) {
+ VerticalScrollMode.SnapScrollMatchHeight(height)
+ } else {
+ VerticalScrollMode.Normal
+ }
+
LazyColumn(
modifier = GlanceModifier.fillMaxSize(),
- verticalScrollMode = VerticalScrollMode.SnapScrollMatchHeight(height)
+ verticalScrollMode = scrollMode
) {
items(pages) { pageFeatures ->
Column(
diff --git a/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTileClickActionCallback.kt b/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTileClickActionCallback.kt
new file mode 100644
index 000000000..824e28447
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTileClickActionCallback.kt
@@ -0,0 +1,29 @@
+package com.sameerasw.essentials.services.widgets
+
+import android.content.Context
+import android.content.Intent
+import androidx.glance.GlanceId
+import androidx.glance.action.ActionParameters
+import androidx.glance.appwidget.action.ActionCallback
+import com.sameerasw.essentials.services.receivers.QsTileActionRouter
+
+class QsTileClickActionCallback : ActionCallback {
+
+ override suspend fun onAction(
+ context: Context,
+ glanceId: GlanceId,
+ parameters: ActionParameters
+ ) {
+ val serviceClassName = parameters[SERVICE_CLASS_KEY] ?: return
+ // action
+ val intent = Intent(context, QsTileActionRouter::class.java).apply {
+ action = QsTileActionRouter.ACTION_TRIGGER_TILE
+ putExtra(QsTileActionRouter.EXTRA_SERVICE_CLASS_NAME, serviceClassName)
+ }
+ context.sendBroadcast(intent)
+ }
+
+ companion object {
+ val SERVICE_CLASS_KEY = ActionParameters.Key("service_class_name")
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTilesWidget.kt b/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTilesWidget.kt
new file mode 100644
index 000000000..f096a2039
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTilesWidget.kt
@@ -0,0 +1,309 @@
+package com.sameerasw.essentials.services.widgets
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.glance.ColorFilter
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.LocalSize
+import androidx.glance.action.actionParametersOf
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.action.actionRunCallback
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.lazy.LazyColumn
+import androidx.glance.appwidget.lazy.VerticalScrollMode
+import androidx.glance.appwidget.lazy.items
+import androidx.glance.appwidget.provideContent
+import androidx.glance.background
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.Column
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxHeight
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.height
+import androidx.glance.layout.padding
+import androidx.glance.layout.size
+import androidx.glance.layout.width
+import androidx.glance.text.FontFamily
+import androidx.glance.text.FontWeight
+import androidx.glance.text.Text
+import androidx.glance.text.TextAlign
+import androidx.glance.text.TextStyle
+import androidx.glance.unit.ColorProvider
+import com.sameerasw.essentials.R
+import com.sameerasw.essentials.data.repository.SettingsRepository
+import com.sameerasw.essentials.services.tiles.QsTileRegistry
+import com.sameerasw.essentials.utils.ColorUtil
+
+class QsTilesWidget : GlanceAppWidget() {
+ override val sizeMode = androidx.glance.appwidget.SizeMode.Exact
+
+ @RequiresApi(Build.VERSION_CODES_FULL.BAKLAVA_1)
+ override suspend fun provideGlance(context: Context, id: GlanceId) {
+ provideContent {
+ GlanceTheme {
+ val prefs = androidx.glance.currentState()
+ val KEY_UPDATE = androidx.datastore.preferences.core.longPreferencesKey("qs_widget_last_update")
+ @Suppress("UNUSED_VARIABLE")
+ val lastUpdate = prefs[KEY_UPDATE] ?: 0L
+
+ val repository = SettingsRepository(context)
+ val pinnedClassNames = repository.getPinnedQsTiles()
+
+ val pinnedTiles = pinnedClassNames.mapNotNull { QsTileRegistry.getTileByClassName(it) }
+ val width = LocalSize.current.width
+ val height = LocalSize.current.height
+
+ Box(
+ modifier = GlanceModifier
+ .fillMaxSize()
+ ) {
+ if (pinnedTiles.isEmpty()) {
+ Column(
+ modifier = GlanceModifier
+ .fillMaxSize()
+ .padding(8.dp)
+ .cornerRadius(16.dp)
+ .background(GlanceTheme.colors.primary)
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = context.getString(R.string.qs_tiles_widget_empty_state),
+ style = TextStyle(
+ color = GlanceTheme.colors.onPrimary,
+ fontSize = 13.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily("google-sans-flex"),
+ textAlign = TextAlign.Center
+ ),
+ modifier = GlanceModifier.fillMaxWidth()
+ )
+ }
+ } else {
+ val columnsCount = if (width >= 300.dp) 2 else 1
+ val targetRowHeight = 65.dp
+ val rowsCount = (height / targetRowHeight).toInt().coerceAtLeast(1)
+ val pageSize = columnsCount * rowsCount
+ val pages = pinnedTiles.chunked(pageSize)
+
+ val halfSpacing = 4.dp
+ val cellHeight = targetRowHeight
+
+ val scrollMode = if (Build.VERSION.SDK_INT >= 37) {
+ VerticalScrollMode.SnapScrollMatchHeight(height)
+ } else {
+ VerticalScrollMode.Normal
+ }
+
+ LazyColumn(
+ modifier = GlanceModifier.fillMaxSize(),
+ verticalScrollMode = scrollMode
+ ) {
+ items(pages) { pageTiles ->
+ Column(
+ modifier = GlanceModifier.fillMaxSize().padding(halfSpacing),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ val rows = pageTiles.chunked(columnsCount)
+ for (rowIndex in 0 until rowsCount) {
+ val rowTiles = rows.getOrNull(rowIndex)
+ if (rowTiles != null) {
+ Row(
+ modifier = GlanceModifier.fillMaxWidth()
+ .height(cellHeight),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ for (colIndex in 0 until columnsCount) {
+ val tile = rowTiles.getOrNull(colIndex)
+ if (tile != null) {
+ val resolvedTitle =
+ context.getString(tile.titleRes)
+ val pastelColor =
+ ColorUtil.getPastelColorFor(
+ resolvedTitle
+ )
+ val vibrantColor =
+ ColorUtil.getVibrantColorFor(
+ resolvedTitle
+ )
+
+ val isActive = QsTileRegistry.isTileActive(
+ context,
+ tile.serviceClass.name
+ )
+
+ val tileBg = if (isActive) {
+ GlanceTheme.colors.primary
+ } else {
+ GlanceTheme.colors.widgetBackground
+ }
+ val tileTextColor = if (isActive) {
+ GlanceTheme.colors.onPrimary
+ } else {
+ GlanceTheme.colors.onSurface
+ }
+ val tileSubtextColor = if (isActive) {
+ GlanceTheme.colors.onPrimary
+ } else {
+ GlanceTheme.colors.onSurfaceVariant
+ }
+
+ val iconBoxSize = 38.dp
+ val iconCornerRadius = 10.dp
+ val iconSize = 22.dp
+ val fontSize = 13.sp
+ val spacerSize = 8.dp
+
+ Box(
+ modifier = GlanceModifier
+ .defaultWeight()
+ .fillMaxHeight()
+ .padding(
+ horizontal = halfSpacing,
+ vertical = halfSpacing
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ val cardModifier = GlanceModifier
+ .fillMaxSize()
+ .cornerRadius(16.dp)
+ .background(tileBg)
+ .clickable(
+ actionRunCallback(
+ actionParametersOf(
+ QsTileClickActionCallback.SERVICE_CLASS_KEY to tile.serviceClass.name
+ )
+ )
+ )
+
+ val activeIconRes = QsTileRegistry.getTileIcon(
+ context,
+ tile.serviceClass.name,
+ tile.iconRes
+ )
+
+ Row(
+ modifier = cardModifier.padding(
+ horizontal = 12.dp,
+ vertical = 6.dp
+ ),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ Box(
+ modifier = GlanceModifier
+ .size(iconBoxSize)
+ .cornerRadius(
+ iconCornerRadius
+ )
+ .background(pastelColor),
+ contentAlignment = Alignment.Center
+ ) {
+ Image(
+ provider = ImageProvider(
+ activeIconRes
+ ),
+ contentDescription = resolvedTitle,
+ colorFilter = ColorFilter.tint(
+ ColorProvider(
+ vibrantColor
+ )
+ ),
+ modifier = GlanceModifier.size(
+ iconSize
+ )
+ )
+ }
+
+ Spacer(
+ modifier = GlanceModifier.width(
+ spacerSize
+ )
+ )
+
+ val subtitle = QsTileRegistry.getTileSubtitle(
+ context,
+ tile.serviceClass.name
+ )
+
+ Column(
+ modifier = GlanceModifier.defaultWeight(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = resolvedTitle,
+ style = TextStyle(
+ color = tileTextColor,
+ fontSize = fontSize,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(
+ "google-sans-flex"
+ ),
+ textAlign = TextAlign.Start
+ ),
+ maxLines = 1
+ )
+ if (subtitle.isNotEmpty()) {
+ Text(
+ text = subtitle,
+ style = TextStyle(
+ color = tileSubtextColor,
+ fontSize = 11.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(
+ "google-sans-flex"
+ ),
+ textAlign = TextAlign.Start
+ ),
+ maxLines = 1
+ )
+ }
+ }
+ }
+ }
+ } else {
+ Spacer(
+ modifier = GlanceModifier
+ .defaultWeight()
+ .fillMaxHeight()
+ .padding(
+ horizontal = halfSpacing,
+ vertical = halfSpacing
+ )
+ )
+ }
+ }
+ }
+ } else {
+ Spacer(
+ modifier = GlanceModifier
+ .fillMaxWidth()
+ .height(cellHeight)
+ .padding(vertical = halfSpacing)
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTilesWidgetReceiver.kt b/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTilesWidgetReceiver.kt
new file mode 100644
index 000000000..140435e2b
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/widgets/QsTilesWidgetReceiver.kt
@@ -0,0 +1,58 @@
+package com.sameerasw.essentials.services.widgets
+
+import android.content.Context
+import android.content.Intent
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import kotlinx.coroutines.launch
+
+class QsTilesWidgetReceiver : GlanceAppWidgetReceiver() {
+ override val glanceAppWidget: GlanceAppWidget = QsTilesWidget()
+
+ override fun onReceive(context: Context, intent: Intent) {
+ super.onReceive(context, intent)
+
+ val action = intent.action
+ if (action == Intent.ACTION_CONFIGURATION_CHANGED ||
+ action == android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE ||
+ action == "com.sameerasw.essentials.action.QS_TILES_WIDGET_UPDATE"
+ ) {
+ kotlinx.coroutines.MainScope().launch {
+ try {
+ val glanceAppWidgetManager =
+ androidx.glance.appwidget.GlanceAppWidgetManager(context)
+ if (action == Intent.ACTION_CONFIGURATION_CHANGED) {
+ kotlinx.coroutines.delay(500)
+ }
+
+ val glanceIds = glanceAppWidgetManager.getGlanceIds(QsTilesWidget::class.java)
+ val timestamp = System.currentTimeMillis()
+ glanceIds.forEach { glanceId ->
+ androidx.glance.appwidget.state.updateAppWidgetState(context, glanceId) { prefs ->
+ val KEY_UPDATE = androidx.datastore.preferences.core.longPreferencesKey("qs_widget_last_update")
+ prefs[KEY_UPDATE] = timestamp
+ }
+ glanceAppWidget.update(context, glanceId)
+ }
+
+ // delay for qs status
+ val updatedGlanceIds = glanceAppWidgetManager.getGlanceIds(QsTilesWidget::class.java)
+ val newTimestamp = System.currentTimeMillis()
+ updatedGlanceIds.forEach { glanceId ->
+ androidx.glance.appwidget.state.updateAppWidgetState(context, glanceId) { prefs ->
+ val KEY_UPDATE = androidx.datastore.preferences.core.longPreferencesKey("qs_widget_last_update")
+ prefs[KEY_UPDATE] = newTimestamp
+ }
+ glanceAppWidget.update(context, glanceId)
+ }
+ } catch (e: Exception) {
+ android.util.Log.e(
+ "QsTilesWidgetReceiver",
+ "Error updating QS tiles widget on broadcast",
+ e
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/QsTilesWidgetConfigurationActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/QsTilesWidgetConfigurationActivity.kt
new file mode 100644
index 000000000..e89d123f0
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/QsTilesWidgetConfigurationActivity.kt
@@ -0,0 +1,32 @@
+package com.sameerasw.essentials.ui.activities
+
+import android.app.Activity
+import android.appwidget.AppWidgetManager
+import android.content.Intent
+import android.os.Bundle
+import com.sameerasw.essentials.FeatureSettingsActivity
+
+class QsTilesWidgetConfigurationActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val appWidgetId = intent?.extras?.getInt(
+ AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID
+ ) ?: AppWidgetManager.INVALID_APPWIDGET_ID
+
+ val resultValue = Intent().apply {
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ }
+ setResult(RESULT_OK, resultValue)
+
+ val settingsIntent = Intent(this, FeatureSettingsActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ putExtra("feature", "Quick settings tiles")
+ }
+ startActivity(settingsIntent)
+
+ finish()
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryAppsTabContent.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryAppsTabContent.kt
index b64f71b26..2657fc92c 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryAppsTabContent.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryAppsTabContent.kt
@@ -22,6 +22,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
@@ -56,7 +57,11 @@ fun BatteryAppsTabContent(
onToggleShowAll: () -> Unit,
showPercentage: Boolean,
onToggleUnit: () -> Unit,
- view: View
+ view: View,
+ currentLevel: Int = 100,
+ chargeTimeRemainingMs: Long? = null,
+ avgCurrentMa: Int? = null,
+ isPlugged: Boolean = false
) {
if (isLoadingAdvanced) {
RoundedCardContainer(modifier = Modifier.fillMaxWidth()) {
@@ -74,6 +79,8 @@ fun BatteryAppsTabContent(
val displayedApps = if (showAllApps) usageApps else usageApps.take(20)
val totalMah = usageApps.sumOf { it.powerMah }.coerceAtLeast(0.0001)
+
+
RoundedCardContainer(modifier = Modifier.fillMaxWidth()) {
displayedApps.forEach { app ->
val displayValue = if (showPercentage) {
@@ -87,6 +94,11 @@ fun BatteryAppsTabContent(
var showMenu by remember { mutableStateOf(false) }
var translationSheetKey by remember { mutableStateOf(null) }
+ val context = LocalContext.current
+ val canOpenAppInfo = remember(app.packageName) {
+ app.packageName != null && app.packageName.contains(".")
+ }
+
Box {
Row(
modifier = Modifier
@@ -100,7 +112,7 @@ fun BatteryAppsTabContent(
HapticUtil.performVirtualKeyHaptic(view)
onToggleUnit()
},
- onLongClick = if (isTranslationModeActive) {
+ onLongClick = if (isTranslationModeActive || canOpenAppInfo) {
{
HapticUtil.performVirtualKeyHaptic(view)
showMenu = true
@@ -168,13 +180,38 @@ fun BatteryAppsTabContent(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
- com.sameerasw.essentials.translation.ui.TranslationMenuItems(
- title = app.appName,
- onSelectKey = { key ->
- showMenu = false
- translationSheetKey = key
- }
- )
+ if (canOpenAppInfo && app.packageName != null) {
+ com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem(
+ text = { Text(stringResource(R.string.label_app_info)) },
+ onClick = {
+ showMenu = false
+ try {
+ val intent = android.content.Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = android.net.Uri.fromParts("package", app.packageName, null)
+ addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ context.startActivity(intent)
+ } catch (_: Exception) {
+ }
+ },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_info_24),
+ contentDescription = null
+ )
+ }
+ )
+ }
+
+ if (isTranslationModeActive) {
+ com.sameerasw.essentials.translation.ui.TranslationMenuItems(
+ title = app.appName,
+ onSelectKey = { key ->
+ showMenu = false
+ translationSheetKey = key
+ }
+ )
+ }
}
}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryCommonComponents.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryCommonComponents.kt
index 360c301d3..1d9f1ccc8 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryCommonComponents.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryCommonComponents.kt
@@ -19,6 +19,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.Image
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.core.graphics.drawable.toBitmap
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonGroupDefaults
@@ -36,6 +39,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.PathEffect
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.foundation.Canvas
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -47,6 +56,8 @@ import com.sameerasw.essentials.ui.theme.Shapes
import com.sameerasw.essentials.utils.HapticUtil
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
+import androidx.compose.material3.Card
+import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer
import java.util.Locale
@OptIn(ExperimentalFoundationApi::class)
@@ -245,6 +256,104 @@ fun BatteryLoadingIndicatorCard() {
}
}
+@Composable
+fun TopAppsBreakdownHeader(
+ usageApps: List
+) {
+ if (usageApps.isEmpty()) return
+
+ val totalAllMah = remember(usageApps) { usageApps.sumOf { it.powerMah }.coerceAtLeast(0.0001) }
+ val majorApps = remember(usageApps, totalAllMah) {
+ usageApps.filter { (it.powerMah / totalAllMah) * 100.0 >= 2.5 }
+ }
+ val remainingApps = remember(usageApps, majorApps) {
+ usageApps.filterNot { majorApps.contains(it) }
+ }
+ val otherMah = remember(remainingApps) { remainingApps.sumOf { it.powerMah } }
+
+ val hasOther = otherMah > 0.0001
+ val totalSegments = majorApps.size + (if (hasOther) 1 else 0)
+ val context = androidx.compose.ui.platform.LocalContext.current
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(88.dp)
+ .padding(vertical = 4.dp),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(40.dp),
+ horizontalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ majorApps.forEachIndexed { index, app ->
+ val weight = ((app.powerMah / totalAllMah) * 100.0).toFloat().coerceAtLeast(1f)
+ var brandColor by remember(app.packageName) {
+ mutableStateOf(null)
+ }
+
+ androidx.compose.runtime.LaunchedEffect(app.packageName) {
+ if (app.packageName != null) {
+ com.sameerasw.essentials.utils.AppUtil.getAppBrandColor(context, app.packageName) { argb ->
+ if (argb != android.graphics.Color.TRANSPARENT && argb != android.graphics.Color.GRAY) {
+ brandColor = Color(argb)
+ }
+ }
+ }
+ }
+
+ val barColor = brandColor ?: MaterialTheme.colorScheme.primaryContainer
+
+ val shape = when {
+ totalSegments == 1 -> CircleShape
+ index == 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes().shape
+ index == totalSegments - 1 -> ButtonGroupDefaults.connectedTrailingButtonShapes().shape
+ else -> ButtonGroupDefaults.connectedMiddleButtonShapes().shape
+ }
+
+ Box(
+ modifier = Modifier
+ .weight(weight)
+ .fillMaxHeight()
+ .clip(shape)
+ .background(barColor),
+ contentAlignment = Alignment.Center
+ ) {
+ if (app.icon != null) {
+ val bitmap = remember(app.icon) { app.icon.toBitmap(48, 48).asImageBitmap() }
+ Image(
+ bitmap = bitmap,
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
+ )
+ } else {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_info_24),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onPrimaryContainer,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ }
+ }
+
+ if (hasOther) {
+ val otherWeight = ((otherMah / totalAllMah) * 100.0).toFloat().coerceAtLeast(1f)
+ val otherShape = if (majorApps.isEmpty()) CircleShape else ButtonGroupDefaults.connectedTrailingButtonShapes().shape
+ Box(
+ modifier = Modifier
+ .weight(otherWeight)
+ .fillMaxHeight()
+ .clip(otherShape)
+ .background(MaterialTheme.colorScheme.outlineVariant)
+ )
+ }
+ }
+ }
+}
+
@Composable
fun BatteryUsageBreakdownHeader(
appsPct: Float,
@@ -345,3 +454,4 @@ private fun BreakdownLegendItem(
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
}
+
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryInfoTabContent.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryInfoTabContent.kt
index a60b2f9d0..bbdcbcb29 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryInfoTabContent.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/battery/BatteryInfoTabContent.kt
@@ -484,6 +484,61 @@ fun BatteryInfoTabContent(
)
}
}
+
+ val isShellAvailableForReset = com.sameerasw.essentials.utils.ShellUtils.isAvailable(context) && com.sameerasw.essentials.utils.ShellUtils.hasPermission(context)
+ if (isShellAvailableForReset) {
+ RoundedCardContainer(modifier = Modifier.fillMaxWidth()) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ MaterialTheme.colorScheme.surfaceBright,
+ shape = Shapes.extraSmall
+ )
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.label_reset_battery_stats_title),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ Text(
+ text = stringResource(R.string.label_reset_battery_stats_desc),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ Button(
+ onClick = {
+ com.sameerasw.essentials.utils.HapticUtil.performVirtualKeyHaptic(view)
+ if (com.sameerasw.essentials.utils.BatteryStatsUtil.resetStats(context)) {
+ onRefresh()
+ }
+ },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(text = stringResource(R.string.action_reset_battery_stats))
+ }
+ }
+
+ var autoResetEnabled by remember {
+ mutableStateOf(settingsRepository.getBoolean("auto_reset_battery_stats", false))
+ }
+ com.sameerasw.essentials.ui.components.cards.IconToggleItem(
+ title = stringResource(R.string.label_auto_reset_battery_stats_title),
+ description = stringResource(R.string.label_auto_reset_battery_stats_desc),
+ isChecked = autoResetEnabled,
+ onCheckedChange = { isChecked ->
+ autoResetEnabled = isChecked
+ settingsRepository.putBoolean("auto_reset_battery_stats", isChecked)
+ },
+ iconRes = R.drawable.rounded_cycle_24
+ )
+ }
+ }
}
// Permission Grant Card at the very bottom if BATTERY_STATS permission is missing AND Shizuku/Root is available & permitted
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BatteryDetailsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BatteryDetailsBottomSheet.kt
index cdf8c19a9..3ef636914 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BatteryDetailsBottomSheet.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BatteryDetailsBottomSheet.kt
@@ -223,6 +223,10 @@ fun BatteryDetailsBottomSheet(
color = MaterialTheme.colorScheme.onSurface
)
}
+ } else if (selectedTab == 1) {
+ com.sameerasw.essentials.ui.components.battery.TopAppsBreakdownHeader(
+ usageApps = usageApps
+ )
} else {
com.sameerasw.essentials.ui.components.battery.BatteryUsageBreakdownHeader(
appsPct = appsPct,
@@ -269,7 +273,11 @@ fun BatteryDetailsBottomSheet(
onToggleShowAll = { showAllApps = !showAllApps },
showPercentage = showPercentage,
onToggleUnit = { showPercentage = !showPercentage },
- view = view
+ view = view,
+ currentLevel = batteryDetails.level,
+ chargeTimeRemainingMs = batteryDetails.chargeTimeRemainingMs,
+ avgCurrentMa = batteryDetails.currentAvgMa,
+ isPlugged = batteryDetails.plugged > 0
)
2 -> BatterySystemTabContent(
isLoadingAdvanced = isLoadingAdvanced,
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt
index 39a5e4222..3818acefc 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt
@@ -1466,6 +1466,11 @@ private fun SearchResultsSection(
if (targetFeatureKey == "LiveWallpaper") "live" else "daily"
)
}
+ } else if (targetFeatureKey == "App updates") {
+ Intent(
+ context,
+ com.sameerasw.essentials.ui.activities.YourAndroidActivity::class.java
+ )
} else {
Intent(
context,
@@ -1492,6 +1497,11 @@ private fun SearchResultsSection(
if (result.featureKey == "LiveWallpaper") "live" else "daily"
)
}
+ } else if (result.featureKey == "App updates") {
+ Intent(
+ context,
+ com.sameerasw.essentials.ui.activities.YourAndroidActivity::class.java
+ )
} else {
Intent(context, FeatureSettingsActivity::class.java).apply {
putExtra("feature", result.featureKey)
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 aa953d2b9..4596adc4b 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
@@ -293,7 +293,8 @@ fun QuickSettingsTilesSettingsUI(
RefreshRateTileService::class.java,
listOf("SHIZUKU"),
R.string.about_desc_refresh_rate_tile,
- R.string.cat_visuals
+ R.string.cat_visuals,
+ isSupported = { _ -> DeviceUtils.isGoogleDevice() }
),
QSTileInfo(
R.string.feat_maps_power_saving_title,
@@ -483,6 +484,9 @@ fun QuickSettingsTilesSettingsUI(
it.contains(tile.serviceClass.name)
}
+ val pinnedQsTiles by viewModel.pinnedQsTileKeys
+ val isPinned = pinnedQsTiles.contains(tile.serviceClass.name)
+
QSTileCard(
tile = tile,
modifier = Modifier
@@ -495,6 +499,7 @@ fun QuickSettingsTilesSettingsUI(
),
isMissingPermissions = !allPermissionsGranted,
isAdded = isAdded,
+ isPinned = isPinned,
onClick = {
if (!allPermissionsGranted) {
selectedTileForPermissions = tile
@@ -529,6 +534,9 @@ fun QuickSettingsTilesSettingsUI(
}
}
},
+ onPinToggle = {
+ viewModel.togglePinQsTile(tile.serviceClass.name)
+ },
onHelpClick = if (tile.aboutDescription != null) {
{
selectedHelpTile = tile
@@ -552,6 +560,104 @@ fun QuickSettingsTilesSettingsUI(
modifier = Modifier.padding(top = 8.dp)
)
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Favorites Widget Section
+ val pinnedQsTiles by viewModel.pinnedQsTileKeys
+ val pinnedTileInfos = remember(pinnedQsTiles, tiles) {
+ pinnedQsTiles.mapNotNull { className -> tiles.find { it.serviceClass.name == className } }
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.padding(start = 12.dp)
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_widgets_24),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(26.dp)
+ )
+ Text(
+ text = stringResource(R.string.qs_tiles_widget_name),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+
+ Text(
+ text = "Long press tiles to add",
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(start = 12.dp)
+ )
+
+ if (pinnedTileInfos.isEmpty()) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp))
+ .background(MaterialTheme.colorScheme.surfaceContainerLow)
+ .padding(24.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = stringResource(R.string.qs_tiles_widget_empty_state),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ } else {
+ pinnedTileInfos.chunked(2).forEach { rowTiles ->
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ rowTiles.forEach { tile ->
+ val allPermissionsGranted = tile.permissionKeys.all { key ->
+ PermissionUIHelper.getPermissionItem(
+ key,
+ context,
+ viewModel
+ )?.isGranted == true
+ }
+ val addedTiles by viewModel.addedQSTiles
+ val componentName = ComponentName(context, tile.serviceClass)
+ val isAdded = addedTiles.any {
+ it.contains(componentName.flattenToString()) ||
+ it.contains(componentName.flattenToShortString()) ||
+ it.contains(tile.serviceClass.name)
+ }
+
+ QSTileCard(
+ tile = tile,
+ modifier = Modifier.weight(1f),
+ isMissingPermissions = !allPermissionsGranted,
+ isAdded = isAdded,
+ isPinned = true,
+ onClick = {
+ viewModel.togglePinQsTile(tile.serviceClass.name)
+ },
+ onPinToggle = {
+ viewModel.togglePinQsTile(tile.serviceClass.name)
+ }
+ )
+ }
+ if (rowTiles.size < 2) {
+ Spacer(modifier = Modifier.weight(1f))
+ }
+ }
+ }
+ }
+ }
+
Spacer(modifier = Modifier.height(contentPadding.calculateBottomPadding()))
}
}
@@ -561,8 +667,10 @@ fun QSTileCard(
tile: QSTileInfo,
isMissingPermissions: Boolean,
isAdded: Boolean,
+ isPinned: Boolean = false,
modifier: Modifier = Modifier,
onClick: () -> Unit,
+ onPinToggle: (() -> Unit)? = null,
onHelpClick: (() -> Unit)? = null
) {
val view = LocalView.current
@@ -612,7 +720,7 @@ fun QSTileCard(
onClick()
},
onLongClick = {
- if (onHelpClick != null) {
+ if (onHelpClick != null || onPinToggle != null) {
com.sameerasw.essentials.utils.HapticUtil.performVirtualKeyHaptic(view)
showMenu = true
}
@@ -638,7 +746,7 @@ fun QSTileCard(
modifier = Modifier.padding(8.dp)
)
- Column {
+ Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(tile.titleRes),
style = MaterialTheme.typography.titleMedium,
@@ -656,28 +764,65 @@ fun QSTileCard(
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
+
+ if (isPinned) {
+ Icon(
+ painter = painterResource(id = R.drawable.round_star_24),
+ contentDescription = null,
+ tint = contentColor,
+ modifier = Modifier
+ .size(16.dp)
+ .padding(top = 2.dp)
+ )
+ }
}
- if (onHelpClick != null) {
+ if (onHelpClick != null || onPinToggle != null) {
com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
- com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem(
- text = {
- Text(stringResource(R.string.action_what_is_this))
- },
- onClick = {
- showMenu = false
- onHelpClick()
- },
- leadingIcon = {
- Icon(
- painter = painterResource(id = R.drawable.rounded_help_24),
- contentDescription = null
- )
- }
- )
+ if (onPinToggle != null) {
+ com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem(
+ text = {
+ Text(
+ if (isPinned) stringResource(R.string.action_unpin)
+ else stringResource(R.string.action_pin)
+ )
+ },
+ onClick = {
+ showMenu = false
+ onPinToggle()
+ },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(
+ id = if (isPinned) R.drawable.rounded_bookmark_remove_24
+ else R.drawable.rounded_bookmark_24
+ ),
+ contentDescription = null
+ )
+ }
+ )
+ }
+
+ if (onHelpClick != null) {
+ com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem(
+ text = {
+ Text(stringResource(R.string.action_what_is_this))
+ },
+ onClick = {
+ showMenu = false
+ onHelpClick()
+ },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_help_24),
+ contentDescription = null
+ )
+ }
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/StatusBarIconSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/StatusBarIconSettingsUI.kt
index 6ae7a8314..c06281a91 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/StatusBarIconSettingsUI.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/StatusBarIconSettingsUI.kt
@@ -318,6 +318,49 @@ fun StatusBarIconSettingsUI(
modifier = Modifier.highlight(highlightSetting == "clock_seconds")
)
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ color = MaterialTheme.colorScheme.surfaceBright,
+ shape = androidx.compose.foundation.shape.RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd)
+ )
+ .highlight(highlightSetting == "clock_position")
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp, start = 20.dp, end = 16.dp, bottom = 2.dp),
+ verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(R.string.stb_clock_position),
+ style = MaterialTheme.typography.titleSmall,
+ color = if (isPermissionGranted) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface.copy(
+ alpha = 0.38f
+ )
+ )
+ }
+ SegmentedPicker(
+ items = listOf(1, 2, 3),
+ selectedItem = viewModel.clockPosition.value,
+ onItemSelected = { pos ->
+ viewModel.setClockPosition(pos, context)
+ },
+ labelProvider = { pos ->
+ when (pos) {
+ 1 -> context.getString(R.string.stb_clock_position_left)
+ 2 -> context.getString(R.string.stb_clock_position_center)
+ 3 -> context.getString(R.string.stb_clock_position_right)
+ else -> ""
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .then(if (!isPermissionGranted) Modifier.alpha(0.5f) else Modifier),
+ )
+ }
+
IconToggleItem(
iconRes = R.drawable.rounded_security_24,
title = stringResource(R.string.stb_privacy_chip),
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/BatteryHistoryManager.kt b/app/src/main/java/com/sameerasw/essentials/utils/BatteryHistoryManager.kt
new file mode 100644
index 000000000..b57e21e1a
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/utils/BatteryHistoryManager.kt
@@ -0,0 +1,70 @@
+package com.sameerasw.essentials.utils
+
+import android.content.Context
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.sameerasw.essentials.data.repository.SettingsRepository
+
+data class BatteryHistoryPoint(
+ val timestamp: Long,
+ val level: Int,
+ val isPlugged: Boolean
+)
+
+object BatteryHistoryManager {
+ private const val KEY_HISTORY_POINTS = "battery_history_points"
+ private const val HOURLY_INTERVAL_MS = 45 * 60 * 1000L // 45 minutes
+
+ fun getHistory(context: Context): List {
+ val repo = SettingsRepository(context)
+ val json = repo.getString(KEY_HISTORY_POINTS, "[]")
+ val lastResetTime = repo.getLong("last_battery_stats_reset_time", -1L)
+
+ val list = try {
+ val type = object : TypeToken>() {}.type
+ Gson().fromJson>(json, type) ?: emptyList()
+ } catch (_: Exception) {
+ emptyList()
+ }
+
+ val filtered = if (lastResetTime > 0L) {
+ list.filter { it.timestamp >= lastResetTime }
+ } else {
+ list
+ }
+
+ return filtered.sortedBy { it.timestamp }
+ }
+
+ fun recordPoint(context: Context, level: Int, isPlugged: Boolean, forceRecord: Boolean = false) {
+ val repo = SettingsRepository(context)
+ val now = System.currentTimeMillis()
+ val currentHistory = getHistory(context).toMutableList()
+
+ if (currentHistory.isNotEmpty()) {
+ val lastPoint = currentHistory.last()
+ val stateChanged = lastPoint.isPlugged != isPlugged
+ val timeElapsed = now - lastPoint.timestamp >= HOURLY_INTERVAL_MS
+
+ if (!forceRecord && !stateChanged && !timeElapsed) {
+ return
+ }
+ }
+
+ currentHistory.add(BatteryHistoryPoint(now, level, isPlugged))
+
+ // Keep last 100
+ val trimmedHistory = if (currentHistory.size > 100) {
+ currentHistory.takeLast(100)
+ } else {
+ currentHistory
+ }
+
+ repo.putString(KEY_HISTORY_POINTS, Gson().toJson(trimmedHistory))
+ }
+
+ fun clearHistory(context: Context) {
+ val repo = SettingsRepository(context)
+ repo.putString(KEY_HISTORY_POINTS, "[]")
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/BatteryInfoUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/BatteryInfoUtil.kt
index 4571f1c12..2614547d9 100644
--- a/app/src/main/java/com/sameerasw/essentials/utils/BatteryInfoUtil.kt
+++ b/app/src/main/java/com/sameerasw/essentials/utils/BatteryInfoUtil.kt
@@ -100,10 +100,15 @@ object BatteryInfoUtil {
val rawEnergy = bm?.getLongProperty(BatteryManager.BATTERY_PROPERTY_ENERGY_COUNTER)?.takeIf { it != Long.MIN_VALUE }
val remainingEnergyMwh = rawEnergy?.let { it / 1_000_000 } // nWh to mWh
- val chargeTimeRemaining = if (android.os.Build.VERSION.SDK_INT >= 28) {
+ val isPlugged = plugged > 0 || status == BatteryManager.BATTERY_STATUS_CHARGING
+ val computedChargeTime = if (android.os.Build.VERSION.SDK_INT >= 28) {
bm?.computeChargeTimeRemaining()?.takeIf { it >= 0 }
} else null
+ val chargeTimeRemaining = if (isPlugged) {
+ computedChargeTime
+ } else null
+
val hasStatsPerm = hasBatteryStatsPermission(context)
var stateOfHealth: Int? = null
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/BatteryStatsUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/BatteryStatsUtil.kt
index 8f95e90a2..bd819e53d 100644
--- a/app/src/main/java/com/sameerasw/essentials/utils/BatteryStatsUtil.kt
+++ b/app/src/main/java/com/sameerasw/essentials/utils/BatteryStatsUtil.kt
@@ -4,6 +4,7 @@ import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import com.sameerasw.essentials.R
+import com.sameerasw.essentials.data.repository.SettingsRepository
data class BatteryUsageApp(
val uid: Int,
@@ -24,6 +25,17 @@ data class CpuWakeupItem(
object BatteryStatsUtil {
+ fun resetStats(context: Context): Boolean {
+ val res1 = ShellUtils.runCommandWithOutput(context, "dumpsys batterystats --reset")
+ ShellUtils.runCommand(context, "cmd battery reset")
+ if (res1 != null) {
+ val repo = SettingsRepository(context)
+ repo.putLong("last_battery_stats_reset_time", System.currentTimeMillis())
+ BatteryHistoryManager.clearHistory(context)
+ }
+ return res1 != null
+ }
+
fun parseUsageApps(context: Context): List {
val output = ShellUtils.runCommandWithOutput(context, "dumpsys batterystats --usage")
?: return emptyList()
@@ -36,13 +48,45 @@ object BatteryStatsUtil {
var currentFg = 0L
var currentBg = 0L
+ fun getSystemUidLabel(uid: Int, pkg: String?): String {
+ return when (uid) {
+ -5 -> "Tethering & Hotspot"
+ 0 -> "Root / Kernel"
+ 1001 -> "Telephony"
+ 1003 -> "Graphics / GPU"
+ 1010 -> "Wi-Fi"
+ 1013 -> "Media Server"
+ 1017 -> "Keystore"
+ 1019 -> "DRM"
+ 1020 -> "Multicast DNS"
+ 1021 -> "GPS"
+ 1036 -> "Log Daemon"
+ 1040 -> "Media Extractor"
+ 1041 -> "Audio Server"
+ 1046 -> "Media Codec"
+ 1047 -> "Camera Server"
+ 1053 -> "WebView Zygote"
+ 1058 -> "Crash Dumps"
+ 1064 -> "Hardware Security"
+ 1066 -> "Stats Daemon"
+ 1067 -> "Incident Daemon"
+ 1069 -> "Low Memory Killer"
+ 1072 -> "GPU Service"
+ 1080 -> "Context Hub"
+ 1082 -> "ART Service"
+ 1083 -> "UWB Subsystem"
+ 1092 -> "PRNG Seeder"
+ else -> pkg?.let { getAppName(pm, it) } ?: "System ($uid)"
+ }
+ }
+
output.lines().forEach { line ->
val trimmed = line.trim()
if (trimmed.startsWith("UID ")) {
val uid = currentUid
if (uid != null && currentMah > 0.0001) {
val pkg = pm.getPackagesForUid(uid)?.firstOrNull()
- val label = pkg?.let { getAppName(pm, it) } ?: "UID $uid"
+ val label = getSystemUidLabel(uid, pkg)
val drawable = pkg?.let { getAppIcon(pm, it) }
list.add(BatteryUsageApp(uid, pkg, label, currentMah, currentFg, currentBg, drawable))
}
@@ -65,7 +109,7 @@ object BatteryStatsUtil {
val lastUid = currentUid
if (lastUid != null && currentMah > 0.0001) {
val pkg = pm.getPackagesForUid(lastUid)?.firstOrNull()
- val label = pkg?.let { getAppName(pm, it) } ?: "UID $lastUid"
+ val label = getSystemUidLabel(lastUid, pkg)
val drawable = pkg?.let { getAppIcon(pm, it) }
list.add(BatteryUsageApp(lastUid, pkg, label, currentMah, currentFg, currentBg, drawable))
}
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt
index 854e8a0b8..c11e86f56 100644
--- a/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt
+++ b/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt
@@ -32,17 +32,32 @@ data class DeviceInfo(
)
object DeviceUtils {
- fun isGoogleDevice(): Boolean {
- return Build.MANUFACTURER.equals("google", ignoreCase = true) ||
+ private val isGoogleDeviceLazy: Boolean by lazy {
+ Build.MANUFACTURER.equals("google", ignoreCase = true) ||
Build.BRAND.equals("google", ignoreCase = true) ||
Build.PRODUCT.contains("pixel", ignoreCase = true)
}
- fun isSamsungDevice(): Boolean {
- return Build.MANUFACTURER.equals("samsung", ignoreCase = true) ||
+ private val isSamsungDeviceLazy: Boolean by lazy {
+ Build.MANUFACTURER.equals("samsung", ignoreCase = true) ||
Build.BRAND.equals("samsung", ignoreCase = true)
}
+ private val isMediatekDeviceLazy: Boolean by lazy {
+ val hardware = Build.HARDWARE.lowercase()
+ val board = Build.BOARD.lowercase()
+ val brand = Build.BRAND.lowercase()
+ val manufacturer = Build.MANUFACTURER.lowercase()
+
+ hardware.contains("mt") || hardware.contains("mediatek") || hardware.contains("dimensity") ||
+ board.contains("mt") || board.contains("mediatek") || board.contains("dimensity") ||
+ brand.contains("mediatek") || manufacturer.contains("mediatek")
+ }
+
+ fun isGoogleDevice(): Boolean = isGoogleDeviceLazy
+ fun isSamsungDevice(): Boolean = isSamsungDeviceLazy
+ fun isMediatekDevice(): Boolean = isMediatekDeviceLazy
+
fun getDeviceInfo(context: Context): DeviceInfo {
val deviceName = try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt
index 0577e0ffe..9e49e4b75 100644
--- a/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt
+++ b/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt
@@ -118,7 +118,7 @@ object FlashlightUtil {
)
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || DeviceUtils.isMediatekDevice()) {
return safeSetTorchMode(cameraManager, cameraId, toLevel > 0)
}
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/ShizukuUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/ShizukuUtils.kt
index 2d292d09c..174568224 100644
--- a/app/src/main/java/com/sameerasw/essentials/utils/ShizukuUtils.kt
+++ b/app/src/main/java/com/sameerasw/essentials/utils/ShizukuUtils.kt
@@ -121,4 +121,16 @@ object ShizukuUtils {
null
}
}
+
+ fun stopShizuku(context: android.content.Context) {
+ try {
+ val intent = android.content.Intent("moe.shizuku.privileged.api.STOP").apply {
+ `package` = "moe.shizuku.privileged.api"
+ addFlags(android.content.Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
+ }
+ context.sendBroadcast(intent)
+ } catch (e: Exception) {
+ android.util.Log.e("ShizukuUtils", "Failed to stop Shizuku", e)
+ }
+ }
}
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 8ad672035..e511451e0 100644
--- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
+++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
@@ -83,6 +83,7 @@ class MainViewModel : ViewModel() {
val isShizukuPermissionGranted = mutableStateOf(false)
val isShizukuAvailable = mutableStateOf(false)
val pinnedFeatureKeys = mutableStateOf>(emptyList())
+ val pinnedQsTileKeys = mutableStateOf>(emptyList())
val isNotificationListenerEnabled = mutableStateOf(false)
val isMapsPowerSavingEnabled = mutableStateOf(false)
val isNotificationLightingEnabled = mutableStateOf(false)
@@ -1544,6 +1545,7 @@ class MainViewModel : ViewModel() {
isPreReleaseCheckEnabled.value =
settingsRepository.getBoolean(SettingsRepository.KEY_CHECK_PRE_RELEASES_ENABLED)
pinnedFeatureKeys.value = settingsRepository.getPinnedFeatures()
+ pinnedQsTileKeys.value = settingsRepository.getPinnedQsTiles()
isLikeSongToastEnabled.value =
settingsRepository.getBoolean(SettingsRepository.KEY_LIKE_SONG_TOAST_ENABLED, true)
isLikeSongAodOverlayEnabled.value = settingsRepository.getBoolean(
@@ -1699,6 +1701,24 @@ class MainViewModel : ViewModel() {
}
}
+ fun togglePinQsTile(serviceClassName: String) {
+ val current = pinnedQsTileKeys.value.toMutableList()
+ if (current.contains(serviceClassName)) {
+ current.remove(serviceClassName)
+ } else {
+ current.add(serviceClassName)
+ }
+ pinnedQsTileKeys.value = current
+ settingsRepository.savePinnedQsTiles(current)
+
+ appContext?.let { context ->
+ val intent = Intent("com.sameerasw.essentials.action.QS_TILES_WIDGET_UPDATE").apply {
+ setPackage(context.packageName)
+ }
+ context.sendBroadcast(intent)
+ }
+ }
+
fun setAutoUpdateEnabled(enabled: Boolean, context: Context) {
isAutoUpdateEnabled.value = enabled
settingsRepository.putBoolean(SettingsRepository.KEY_AUTO_UPDATE_ENABLED, enabled)
diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt
index e26111588..0680c8fa6 100644
--- a/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt
+++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt
@@ -24,6 +24,7 @@ class StatusBarIconViewModel : ViewModel() {
val isSmartDataEnabled = mutableStateOf(false)
val selectedNetworkTypes = mutableStateOf(setOf(NetworkType.NETWORK_4G, NetworkType.NETWORK_5G))
val isClockSecondsEnabled = mutableStateOf(false)
+ val clockPosition = mutableStateOf(0)
val batteryPercentageMode = mutableStateOf(0) // 0: Hide, 1: Always, 2: Charging
val isPrivacyChipEnabled = mutableStateOf(true)
val isWriteSettingsEnabled = mutableStateOf(false)
@@ -51,6 +52,7 @@ class StatusBarIconViewModel : ViewModel() {
const val PREF_SMART_DATA_ENABLED = "smart_data_enabled"
const val PREF_SELECTED_NETWORK_TYPES = "selected_network_types"
const val PREF_BATTERY_PERCENT_MODE = "battery_percent_mode"
+ const val PREF_CLOCK_POSITION = "clock_position"
const val PREF_HIDE_SYSTEM_ICONS = "hide_system_icons"
const val PREF_HIDE_SYSTEM_ICONS_LOCKED_ONLY = "hide_system_icons_locked_only"
const val PREF_HIDE_CLOCK = "hide_clock"
@@ -235,7 +237,21 @@ class StatusBarIconViewModel : ViewModel() {
private fun updateIconBlacklist(context: Context) {
if (!isWriteSecureSettingsEnabled.value) return
- val blacklistNames = StatusBarIconRegistry.getBlacklistNames(getIconVisibilities())
+ val blacklistNames = StatusBarIconRegistry.getBlacklistNames(getIconVisibilities()).toMutableSet()
+ when (clockPosition.value) {
+ 1 -> {
+ blacklistNames.add("right_clock_position")
+ blacklistNames.add("middle_clock_position")
+ }
+ 2 -> {
+ blacklistNames.add("left_clock_position")
+ blacklistNames.add("right_clock_position")
+ }
+ 3 -> {
+ blacklistNames.add("left_clock_position")
+ blacklistNames.add("middle_clock_position")
+ }
+ }
updateIconBlacklistSetting(context, blacklistNames)
}
@@ -322,6 +338,7 @@ class StatusBarIconViewModel : ViewModel() {
setAdvancedFlagEnabled(context, PREF_HIDE_CLOCK, false)
setAdvancedFlagEnabled(context, PREF_HIDE_NOTIFICATION_ICONS, false)
setBatteryPercentageMode(0, context)
+ setClockPosition(0, context)
// Build default visibility map
val defaultVisibilities =
@@ -334,6 +351,7 @@ class StatusBarIconViewModel : ViewModel() {
private fun loadStatusBarSettings(context: Context) {
val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE)
batteryPercentageMode.value = prefs.getInt(PREF_BATTERY_PERCENT_MODE, 0)
+ clockPosition.value = prefs.getInt(PREF_CLOCK_POSITION, 0)
// Load Clock Seconds
isClockSecondsEnabled.value =
@@ -350,6 +368,14 @@ class StatusBarIconViewModel : ViewModel() {
) == 1)
}
+ fun setClockPosition(position: Int, context: Context) {
+ clockPosition.value = position
+ context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE).edit {
+ putInt(PREF_CLOCK_POSITION, position)
+ }
+ updateIconBlacklist(context)
+ }
+
fun setClockSecondsEnabled(enabled: Boolean, context: Context) {
isClockSecondsEnabled.value = enabled
updateSettingsValue(context, "clock_seconds", if (enabled) 1 else 0)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 959dc4ed6..44331252a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,4 +1,5 @@
+ App Info
Customization
BETA
Attempt Shizuku restart
@@ -356,9 +357,17 @@
Samsung Smart Features
Samsung Services
Ethernet
+ Network Speed
+ Glove Mode
+ Refresh Rate / Frame Rate
+ No SIM
Show Seconds in Clock
+ Clock Position
+ Left
+ Center
+ Right
Battery Percentage
Always
Charging
@@ -1702,7 +1711,17 @@
Critical Throttling
Emergency Throttling
Shutdown Imminent
- Charging mode QS tile options
+ Options
+ Reset stats
+ Reset Battery Statistics
+ Resets system battery statistics (dumpsys batterystats --reset). This clears historical battery usage and app drain data so it starts tracking fresh from the current charge.
+ Auto reset
+ Automatically reset battery statistics when the device is unplugged above 90% charge.
+ Battery Drain & Prediction
+ Actual drain
+ Predicted
+ %s to go
+ %s ago
CPU wakeup history
Hide CPU wakeups
Screen On Drain
@@ -1838,6 +1857,9 @@
Favorite Features
View and launch your favorite features at a glance.
No favorite features pinned yet. Open the app to pin some!
+ Favorite Tiles
+ Trigger your favorite Quick Settings tiles directly from your home screen.
+ No favorite tiles pinned yet. Open the app to pin some!
control Essentials features
read, update and control Essentials features and settings
diff --git a/app/src/main/res/xml/qs_tiles_widget_info.xml b/app/src/main/res/xml/qs_tiles_widget_info.xml
new file mode 100644
index 000000000..092ff01ef
--- /dev/null
+++ b/app/src/main/res/xml/qs_tiles_widget_info.xml
@@ -0,0 +1,15 @@
+
+