Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
5ba9265
Update source file strings.xml
sameerasw Jan 15, 2026
c7e680f
New translations strings.xml (Romanian)
sameerasw Jan 15, 2026
ae2c9c4
New translations strings.xml (French)
sameerasw Jan 15, 2026
b8fa26c
New translations strings.xml (Spanish)
sameerasw Jan 15, 2026
a53704f
New translations strings.xml (Afrikaans)
sameerasw Jan 15, 2026
5166ee7
New translations strings.xml (Arabic)
sameerasw Jan 15, 2026
a5e88cf
New translations strings.xml (Catalan)
sameerasw Jan 15, 2026
192c585
New translations strings.xml (Czech)
sameerasw Jan 15, 2026
e8b5c77
New translations strings.xml (Danish)
sameerasw Jan 15, 2026
28d22c8
New translations strings.xml (German)
sameerasw Jan 15, 2026
30f6a83
New translations strings.xml (Greek)
sameerasw Jan 15, 2026
3d8642a
New translations strings.xml (Finnish)
sameerasw Jan 15, 2026
1680897
New translations strings.xml (Hebrew)
sameerasw Jan 15, 2026
be9030a
New translations strings.xml (Hungarian)
sameerasw Jan 15, 2026
bed888a
New translations strings.xml (Italian)
sameerasw Jan 15, 2026
cd07524
New translations strings.xml (Japanese)
sameerasw Jan 15, 2026
551168a
New translations strings.xml (Korean)
sameerasw Jan 15, 2026
fa9bda2
New translations strings.xml (Dutch)
sameerasw Jan 15, 2026
a6a6109
New translations strings.xml (Norwegian)
sameerasw Jan 15, 2026
283f168
New translations strings.xml (Polish)
sameerasw Jan 15, 2026
73d6c01
New translations strings.xml (Portuguese)
sameerasw Jan 15, 2026
67569c2
New translations strings.xml (Russian)
sameerasw Jan 15, 2026
2fb71eb
New translations strings.xml (Serbian (Cyrillic))
sameerasw Jan 15, 2026
bb5ab13
New translations strings.xml (Swedish)
sameerasw Jan 15, 2026
38bdcd3
New translations strings.xml (Turkish)
sameerasw Jan 15, 2026
07e1f4c
New translations strings.xml (Ukrainian)
sameerasw Jan 15, 2026
64c0543
New translations strings.xml (Chinese Simplified)
sameerasw Jan 15, 2026
5a2533a
New translations strings.xml (Chinese Traditional)
sameerasw Jan 15, 2026
88662ff
New translations strings.xml (English)
sameerasw Jan 15, 2026
0f20851
New translations strings.xml (Vietnamese)
sameerasw Jan 15, 2026
4301ad8
New translations strings.xml (Portuguese, Brazilian)
sameerasw Jan 15, 2026
39dde87
New translations strings.xml (Sinhala)
sameerasw Jan 15, 2026
d5dca3d
New translations strings.xml (Acholi)
sameerasw Jan 15, 2026
1f042d6
Update source file strings.xml
sameerasw Jan 15, 2026
982dc08
Initial battery widget impleemntation
sameerasw Jan 15, 2026
d67c8c6
AirSync integration with battery
sameerasw Jan 15, 2026
eeac9f2
New translations strings.xml (German)
sameerasw Jan 15, 2026
f435e21
Bluetooth abttery support
sameerasw Jan 16, 2026
0a9e3bc
Batteris widget styling
sameerasw Jan 16, 2026
fd31f71
widget theme simplifications
sameerasw Jan 16, 2026
2f3b3db
New translations strings.xml (Japanese)
sameerasw Jan 16, 2026
02fd15f
Auto update colors with the widget
sameerasw Jan 16, 2026
b224b44
minor color updates
sameerasw Jan 16, 2026
0638c0d
New translations strings.xml (Japanese)
sameerasw Jan 16, 2026
42b112f
visual updates in ui of widget
sameerasw Jan 16, 2026
7896633
Limit max devices in the widget
sameerasw Jan 16, 2026
857fe4b
improve battery ring
sameerasw Jan 16, 2026
bf7ef52
Widget background toggle
sameerasw Jan 16, 2026
d268851
Batteries haptics
sameerasw Jan 16, 2026
f248583
cleanup
sameerasw Jan 16, 2026
1cd033b
Merge pull request #114 from sameerasw/batteries
sameerasw Jan 16, 2026
e6069da
Fix #112 not using the new float values instead of int for notificati…
sameerasw Jan 16, 2026
ac7cb5e
New translations strings.xml (Romanian)
sameerasw Jan 16, 2026
5f139f5
New translations strings.xml (French)
sameerasw Jan 16, 2026
554db71
New translations strings.xml (Spanish)
sameerasw Jan 16, 2026
13f887b
New translations strings.xml (Afrikaans)
sameerasw Jan 16, 2026
a17c486
New translations strings.xml (Arabic)
sameerasw Jan 16, 2026
4d0f026
New translations strings.xml (Catalan)
sameerasw Jan 16, 2026
a7d61b5
New translations strings.xml (Czech)
sameerasw Jan 16, 2026
80ecaf3
New translations strings.xml (Danish)
sameerasw Jan 16, 2026
1e8c7bd
New translations strings.xml (German)
sameerasw Jan 16, 2026
6376f7e
New translations strings.xml (Greek)
sameerasw Jan 16, 2026
f877872
New translations strings.xml (Finnish)
sameerasw Jan 16, 2026
e2497f1
New translations strings.xml (Hebrew)
sameerasw Jan 16, 2026
c60da60
New translations strings.xml (Hungarian)
sameerasw Jan 16, 2026
f12486e
New translations strings.xml (Italian)
sameerasw Jan 16, 2026
c9999ea
New translations strings.xml (Japanese)
sameerasw Jan 16, 2026
5e84b70
New translations strings.xml (Korean)
sameerasw Jan 16, 2026
d5a5e7a
New translations strings.xml (Dutch)
sameerasw Jan 16, 2026
9e8d2db
New translations strings.xml (Norwegian)
sameerasw Jan 16, 2026
6ec8c3a
New translations strings.xml (Polish)
sameerasw Jan 16, 2026
a2c5d22
New translations strings.xml (Portuguese)
sameerasw Jan 16, 2026
78a7c4c
New translations strings.xml (Russian)
sameerasw Jan 16, 2026
77e41cd
New translations strings.xml (Serbian (Cyrillic))
sameerasw Jan 16, 2026
08c1553
New translations strings.xml (Swedish)
sameerasw Jan 16, 2026
176942e
New translations strings.xml (Turkish)
sameerasw Jan 16, 2026
8773530
New translations strings.xml (Ukrainian)
sameerasw Jan 16, 2026
4ddb67f
New translations strings.xml (Chinese Simplified)
sameerasw Jan 16, 2026
e6938a8
New translations strings.xml (Chinese Traditional)
sameerasw Jan 16, 2026
e551088
New translations strings.xml (English)
sameerasw Jan 16, 2026
7f5c5fb
New translations strings.xml (Vietnamese)
sameerasw Jan 16, 2026
f47e98e
New translations strings.xml (Portuguese, Brazilian)
sameerasw Jan 16, 2026
1fffe95
New translations strings.xml (Sinhala)
sameerasw Jan 16, 2026
4c75de2
New translations strings.xml (Acholi)
sameerasw Jan 16, 2026
20df424
Update source file strings.xml
sameerasw Jan 16, 2026
0dd2bb9
New translations strings.xml (Japanese)
sameerasw Jan 16, 2026
2c65f74
version upgrade
sameerasw Jan 16, 2026
9584f5e
Merge pull request #105 from sameerasw/l10n_develop
sameerasw Jan 16, 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
12 changes: 8 additions & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ android {
applicationId = "com.sameerasw.essentials"
minSdk = 26
targetSdk = 36
versionCode = 21
versionName = "10.0"
versionCode = 22
versionName = "10.1"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down Expand Up @@ -56,8 +56,8 @@ dependencies {
// Android 12+ SplashScreen API with backward compatibility attributes
implementation("androidx.core:core-splashscreen:1.0.1")

// Force latest Material3 1.5.0-alpha10 for ToggleButton & ButtonGroup support
implementation("androidx.compose.material3:material3:1.5.0-alpha10")
// Force latest Material3 1.5.0-alpha12 for ToggleButton & ButtonGroup support
implementation("androidx.compose.material3:material3:1.5.0-alpha12")

implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
Expand Down Expand Up @@ -104,4 +104,8 @@ dependencies {

// SymSpell for word suggestions
implementation("com.darkrockstudios:symspellkt:3.4.0")

// Glance for Widgets
implementation("androidx.glance:glance-appwidget:1.1.0")
implementation("androidx.glance:glance-material3:1.1.0")
}
36 changes: 36 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" tools:ignore="ProtectedPermissions" />
<uses-permission android:name="moe.shizuku.manager.permission.API_V23" />
<uses-permission android:name="moe.shizuku.privileged.api.permission.BIND_SHIZUKU_SERVICE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />

<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.POST_PROMOTED_NOTIFICATIONS" tools:ignore="UnusedAttribute" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<permission android:name="com.sameerasw.permission.ESSENTIALS_AIRSYNC_BRIDGE" android:protectionLevel="signature" />
<uses-permission android:name="com.sameerasw.permission.ESSENTIALS_AIRSYNC_BRIDGE" />


<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions" />
Expand Down Expand Up @@ -248,6 +253,28 @@
android:resource="@xml/screen_off_widget_info" />
</receiver>

<receiver
android:name=".services.widgets.BatteriesWidgetReceiver"
android:label="@string/batteries_widget_label"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<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_LOW" />
<action android:name="android.intent.action.BATTERY_OKAY" />
<action android:name="android.intent.action.BATTERY_CHANGED" />
<action android:name="android.bluetooth.device.action.BATTERY_LEVEL_CHANGED" />
<action android:name="android.bluetooth.device.action.ACL_CONNECTED" />
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED" />
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
<action android:name="android.intent.action.CONFIGURATION_CHANGED" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/batteries_widget_info" />
</receiver>

<service
android:name=".services.tiles.CaffeinateTileService"
android:exported="true"
Expand Down Expand Up @@ -505,6 +532,15 @@
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>

<receiver
android:name=".services.receivers.AirSyncBridgeReceiver"
android:exported="true"
android:permission="com.sameerasw.permission.ESSENTIALS_AIRSYNC_BRIDGE">
<intent-filter>
<action android:name="com.sameerasw.essentials.action.UPDATE_MAC_BATTERY" />
</intent-filter>
</receiver>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import com.sameerasw.essentials.ui.composables.configs.ButtonRemapSettingsUI
import com.sameerasw.essentials.ui.composables.configs.DynamicNightLightSettingsUI
import com.sameerasw.essentials.ui.composables.configs.SnoozeNotificationsSettingsUI
import com.sameerasw.essentials.ui.composables.configs.LocationReachedSettingsUI
import com.sameerasw.essentials.ui.composables.configs.BatteriesSettingsUI
import com.sameerasw.essentials.viewmodels.CaffeinateViewModel
import com.sameerasw.essentials.viewmodels.MainViewModel
import com.sameerasw.essentials.viewmodels.StatusBarIconViewModel
Expand Down Expand Up @@ -597,6 +598,12 @@ class FeatureSettingsActivity : FragmentActivity() {
highlightSetting = highlightSetting
)
}
"Batteries" -> {
BatteriesSettingsUI(
viewModel = viewModel,
modifier = Modifier.padding(top = 16.dp)
)
}
// else -> default UI (optional cleanup)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ class SettingsRepository(private val context: Context) {
const val KEY_KEYBOARD_PITCH_BLACK = "keyboard_pitch_black"
const val KEY_KEYBOARD_CLIPBOARD_ENABLED = "keyboard_clipboard_enabled"

// Essentials-AirSync Bridge
const val KEY_AIRSYNC_CONNECTION_ENABLED = "airsync_connection_enabled"
const val KEY_MAC_BATTERY_LEVEL = "mac_battery_level"
const val KEY_MAC_BATTERY_IS_CHARGING = "mac_battery_is_charging"
const val KEY_MAC_BATTERY_LAST_UPDATED = "mac_battery_last_updated"
const val KEY_AIRSYNC_MAC_CONNECTED = "airsync_mac_connected"

const val KEY_BLUETOOTH_DEVICES_BATTERY = "bluetooth_devices_battery"
const val KEY_SHOW_BLUETOOTH_DEVICES = "show_bluetooth_devices"
const val KEY_BATTERY_WIDGET_MAX_DEVICES = "battery_widget_max_devices"
const val KEY_BATTERY_WIDGET_BACKGROUND_ENABLED = "battery_widget_background_enabled"
}

// Observe changes
Expand Down Expand Up @@ -300,8 +311,8 @@ class SettingsRepository(private val context: Context) {
val wrapperMap = mutableMapOf<String, Map<String, Any>>()

p.all.forEach { (key, value) ->
// Skip app lists as requested
if (key.endsWith("_selected_apps") || key == "freeze_auto_excluded_apps") {
// Skip app lists as requested, and stale data
if (key.endsWith("_selected_apps") || key == "freeze_auto_excluded_apps" || key.startsWith("mac_battery_") || key == "airsync_mac_connected") {
return@forEach
}

Expand Down Expand Up @@ -380,4 +391,28 @@ class SettingsRepository(private val context: Context) {
try { inputStream.close() } catch(e: Exception) {}
}
}

fun getBluetoothDevicesBattery(): List<com.sameerasw.essentials.utils.BluetoothBatteryUtils.BluetoothDeviceBattery> {
val json = prefs.getString(KEY_BLUETOOTH_DEVICES_BATTERY, null) ?: return emptyList()
val type = object : TypeToken<List<com.sameerasw.essentials.utils.BluetoothBatteryUtils.BluetoothDeviceBattery>>() {}.type
return try {
gson.fromJson(json, type) ?: emptyList()
} catch (e: Exception) {
emptyList()
}
}

fun saveBluetoothDevicesBattery(devices: List<com.sameerasw.essentials.utils.BluetoothBatteryUtils.BluetoothDeviceBattery>) {
val json = gson.toJson(devices)
putString(KEY_BLUETOOTH_DEVICES_BATTERY, json)
}

fun isBluetoothDevicesEnabled(): Boolean = getBoolean(KEY_SHOW_BLUETOOTH_DEVICES, false)
fun setBluetoothDevicesEnabled(enabled: Boolean) = putBoolean(KEY_SHOW_BLUETOOTH_DEVICES, enabled)

fun getBatteryWidgetMaxDevices(): Int = getInt(KEY_BATTERY_WIDGET_MAX_DEVICES, 8)
fun setBatteryWidgetMaxDevices(count: Int) = putInt(KEY_BATTERY_WIDGET_MAX_DEVICES, count)

fun isBatteryWidgetBackgroundEnabled(): Boolean = getBoolean(KEY_BATTERY_WIDGET_BACKGROUND_ENABLED, true)
fun setBatteryWidgetBackgroundEnabled(enabled: Boolean) = putBoolean(KEY_BATTERY_WIDGET_BACKGROUND_ENABLED, enabled)
}
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,20 @@ object FeatureRegistry {
) {
override fun isEnabled(viewModel: MainViewModel) = true
override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {}
},

object : Feature(
id = "Batteries",
title = R.string.feat_batteries_title,
iconRes = R.drawable.rounded_battery_charging_60_24,
category = R.string.cat_tools,
description = R.string.feat_batteries_desc,
permissionKeys = listOf("BLUETOOTH_CONNECT", "BLUETOOTH_SCAN"),
showToggle = false,
hasMoreSettings = true
) {
override fun isEnabled(viewModel: MainViewModel) = true
override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {}
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ fun initPermissionRegistry() {
PermissionRegistry.register("NOTIFICATION_LISTENER", R.string.feat_maps_power_saving_title)
PermissionRegistry.register("NOTIFICATION_LISTENER", R.string.feat_notification_lighting_title)

// Bluetooth permissions
PermissionRegistry.register("BLUETOOTH_CONNECT", R.string.feat_batteries_title)
PermissionRegistry.register("BLUETOOTH_SCAN", R.string.feat_batteries_title)

// Draw over other apps permission
PermissionRegistry.register("DRAW_OVER_OTHER_APPS", R.string.feat_notification_lighting_title)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,14 @@ class NotificationListener : NotificationListenerService() {
val appSelected = isAppSelectedForNotificationLighting(sbn.packageName)
if (appSelected) {
val cornerRadius = try {
prefs.getInt("edge_lighting_corner_radius", 20)
prefs.getFloat("edge_lighting_corner_radius", 20f)
} catch (e: ClassCastException) {
prefs.getFloat("edge_lighting_corner_radius", 20f).toInt()
prefs.getInt("edge_lighting_corner_radius", 20).toFloat()
}
val strokeThickness = try {
prefs.getInt("edge_lighting_stroke_thickness", 8)
prefs.getFloat("edge_lighting_stroke_thickness", 8f)
} catch (e: ClassCastException) {
prefs.getFloat("edge_lighting_stroke_thickness", 8f).toInt()
prefs.getInt("edge_lighting_stroke_thickness", 8).toFloat()
}
val colorModeName = prefs.getString("edge_lighting_color_mode", NotificationLightingColorMode.SYSTEM.name)
val colorMode = NotificationLightingColorMode.valueOf(colorModeName ?: NotificationLightingColorMode.SYSTEM.name)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.sameerasw.essentials.services.receivers

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.sameerasw.essentials.data.repository.SettingsRepository
import kotlinx.coroutines.launch

class AirSyncBridgeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == "com.sameerasw.essentials.action.UPDATE_MAC_BATTERY") {
val pendingResult = goAsync()
val level = intent.getIntExtra("level", -1)
val isCharging = intent.getBooleanExtra("isCharging", false)
val lastUpdated = intent.getLongExtra("lastUpdated", System.currentTimeMillis())
val isConnected = intent.getBooleanExtra("isConnected", true)

android.util.Log.d("AirSyncBridge", "Received Mac status: level=$level, connected=$isConnected")

val repository = SettingsRepository(context)
if (repository.getBoolean(SettingsRepository.KEY_AIRSYNC_CONNECTION_ENABLED)) {
repository.putInt(SettingsRepository.KEY_MAC_BATTERY_LEVEL, level)
repository.putBoolean(SettingsRepository.KEY_MAC_BATTERY_IS_CHARGING, isCharging)
repository.putLong(SettingsRepository.KEY_MAC_BATTERY_LAST_UPDATED, lastUpdated)
repository.putBoolean(SettingsRepository.KEY_AIRSYNC_MAC_CONNECTED, isConnected)

// Trigger widget update directly
val glanceAppWidgetManager = androidx.glance.appwidget.GlanceAppWidgetManager(context)
// Use IO dispatcher to avoid main thread jank/timeouts
kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch {
try {
// Define keys matching BatteriesWidget
val KEY_AIRSYNC_ENABLED = androidx.datastore.preferences.core.booleanPreferencesKey(com.sameerasw.essentials.data.repository.SettingsRepository.KEY_AIRSYNC_CONNECTION_ENABLED)
val KEY_MAC_LEVEL = androidx.datastore.preferences.core.intPreferencesKey(com.sameerasw.essentials.data.repository.SettingsRepository.KEY_MAC_BATTERY_LEVEL)
val KEY_MAC_CONNECTED = androidx.datastore.preferences.core.booleanPreferencesKey(com.sameerasw.essentials.data.repository.SettingsRepository.KEY_AIRSYNC_MAC_CONNECTED)

val glanceIds = glanceAppWidgetManager.getGlanceIds(com.sameerasw.essentials.services.widgets.BatteriesWidget::class.java)

android.util.Log.d("AirSyncBridge", "Found ${glanceIds.size} widgets to update")

glanceIds.forEach { glanceId ->
androidx.glance.appwidget.state.updateAppWidgetState(context, glanceId) { prefs ->
prefs[KEY_AIRSYNC_ENABLED] = true
prefs[KEY_MAC_LEVEL] = level
prefs[KEY_MAC_CONNECTED] = isConnected
// Add charging state
val KEY_MAC_IS_CHARGING = androidx.datastore.preferences.core.booleanPreferencesKey(com.sameerasw.essentials.data.repository.SettingsRepository.KEY_MAC_BATTERY_IS_CHARGING)
prefs[KEY_MAC_IS_CHARGING] = isCharging
}

android.util.Log.d("AirSyncBridge", "Triggering update for glanceId: $glanceId")
com.sameerasw.essentials.services.widgets.BatteriesWidget().update(context, glanceId)
}
} catch (e: Exception) {
android.util.Log.e("AirSyncBridge", "Error updating widget", e)
} finally {
pendingResult.finish()
}
}
} else {
pendingResult.finish()
}
}
}
}
Loading