Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
41a0cbe
feat: support legacy scroll mode by conditionally setting VerticalScr…
sameerasw Aug 3, 2026
c2bf43d
feat: add Quick Settings tiles home screen widget and headless trigge…
sameerasw Aug 3, 2026
9739b3d
feat: add tile active state detection and update widget UI to reflect…
sameerasw Aug 3, 2026
7808fb7
feat: implement widget update triggers and state refreshing for QsTil…
sameerasw Aug 3, 2026
255ea71
feat: add pinned tile indicator to settings UI and display dynamic su…
sameerasw Aug 3, 2026
6d1ffda
feat: implement dynamic icon resolution for QsTilesWidget via QsTileR…
sameerasw Aug 3, 2026
7b70d8f
refactor: remove redundant widget refresh logic in QsTileClickActionC…
sameerasw Aug 3, 2026
5d46635
feat: implement configuration activity for quick settings tiles widget
sameerasw Aug 3, 2026
163bcf5
fix: add fallback to startActivity for ColorPickerTileService to prev…
sameerasw Aug 3, 2026
7433828
feat: add functionality to reset system battery statistics
sameerasw Aug 4, 2026
8e329f2
feat: add BatteryDrainGraphCard to visualize battery usage and add co…
sameerasw Aug 4, 2026
1ca0d27
refactor: update battery graph UI to display time-based status with i…
sameerasw Aug 4, 2026
bbedb80
feat: new per app battery drian graph
sameerasw Aug 4, 2026
99ee8c6
feat: add automatic battery stats reset with support oif charging info
sameerasw Aug 4, 2026
3134eff
feat: implement battery history tracking and update usage visualizati…
sameerasw Aug 5, 2026
cd731c6
refactor: optimize battery usage chart grouping logic and simplify ch…
sameerasw Aug 5, 2026
1feb8ed
refactor: remove BatteryDrainGraph
sameerasw Aug 5, 2026
6fc7a62
feat: add system UID mapping in battery
sameerasw Aug 5, 2026
eeeb4f1
feat: add app info shortcut to long-press menu in battery apps list
sameerasw Aug 5, 2026
9a53d88
fix: disable torch intensity support for MediaTek devices to prevent …
sameerasw Aug 5, 2026
76092ca
feat: add functionality to stop Shizuku service with Shut up #686
sameerasw Aug 5, 2026
9dacc24
feat: add statusbar clock position setting #582
sameerasw Aug 5, 2026
dc71def
feat: add new status bar icons and update blacklist #588
sameerasw Aug 5, 2026
bb1f5e2
feat: restrict refresh rate settings to Google devices #577
sameerasw Aug 5, 2026
f4f4280
refactor: update App updates navigation to use YourAndroidActivity #583
sameerasw Aug 5, 2026
959d546
chore: bump version code to 58 and version name to 16.6
sameerasw Aug 5, 2026
fa6970b
Merge branch 'main' into develop
sameerasw Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
40 changes: 40 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,36 @@
android:resource="@xml/favorites_widget_info" />
</receiver>

<activity
android:name=".ui.activities.QsTilesWidgetConfigurationActivity"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>

<receiver
android:name=".services.widgets.QsTilesWidgetReceiver"
android:exported="true"
android:label="@string/qs_tiles_widget_name">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.intent.action.CONFIGURATION_CHANGED" />
<action android:name="com.sameerasw.essentials.action.QS_TILES_WIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/qs_tiles_widget_info" />
</receiver>

<receiver
android:name=".services.receivers.QsTileActionRouter"
android:exported="false">
<intent-filter>
<action android:name="com.sameerasw.essentials.action.TRIGGER_QS_TILE" />
</intent-filter>
</receiver>

<service
android:name=".services.tiles.CaffeinateTileService"
android:exported="true"
Expand Down Expand Up @@ -1014,6 +1044,16 @@
</intent-filter>
</receiver>

<receiver
android:name=".services.receivers.BatteryAutomationReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
<action android:name="android.intent.action.ACTION_POWER_DISCONNECTED" />
<action android:name="android.intent.action.BATTERY_CHANGED" />
</intent-filter>
</receiver>

<receiver
android:name=".services.receivers.AppBootReceiver"
android:exported="true">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -914,6 +916,22 @@ class SettingsRepository(private val context: Context) {
putString(KEY_PINNED_FEATURES, json)
}

fun getPinnedQsTiles(): List<String> {
val json = prefs.getString(KEY_PINNED_QS_TILES, null)
return if (json != null) {
try {
gson.fromJson(json, Array<String>::class.java).toList()
} catch (e: Exception) {
emptyList()
}
} else emptyList()
}

fun savePinnedQsTiles(tiles: List<String>) {
val json = gson.toJson(tiles)
putString(KEY_PINNED_QS_TILES, json)
}

fun getRecentSearches(): List<com.sameerasw.essentials.domain.model.SearchableItem> {
val json = prefs.getString(KEY_RECENT_SEARCHES, null)
return if (json != null) {
Expand Down
33 changes: 30 additions & 3 deletions app/src/main/java/com/sameerasw/essentials/domain/StatusBarIcon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
),
Expand Down Expand Up @@ -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(
Expand All @@ -282,7 +289,6 @@ object StatusBarIconRegistry {
id = "samsung_smart",
displayNameRes = R.string.icon_samsung_smart,
blacklistNames = listOf(
"glove",
"gesture",
"smart_scroll",
"face",
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -1338,7 +1340,7 @@ object FeatureRegistry {
context.startActivity(
Intent(
context,
com.sameerasw.essentials.AppUpdatesActivity::class.java
com.sameerasw.essentials.ui.activities.YourAndroidActivity::class.java
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Loading
Loading