feat(monitor): Add expanded notification with battery progress bars

Add distinct expanded (big) notification layouts with visual battery

progress bars for dual pods, single pods, and unknown devices.

Collapsed views remain unchanged. Status icon containers use fixed

width to ensure uniform progress bar lengths across rows.
This commit is contained in:
darken
2026-03-01 00:15:42 +01:00
committed by Matthias Urhahn
parent a47addc8bc
commit eefe740b6f
6 changed files with 403 additions and 2 deletions
@@ -15,6 +15,7 @@ import eu.darken.capod.pods.core.SinglePodDevice
import eu.darken.capod.pods.core.formatBatteryPercent
import eu.darken.capod.pods.core.getBatteryDrawable
import javax.inject.Inject
import kotlin.math.roundToInt
class MonitorNotificationViewFactory @Inject constructor(
@@ -78,9 +79,79 @@ class MonitorNotificationViewFactory @Inject constructor(
private fun createUnknownDevice(device: PodDevice): RemoteViews = RemoteViews(
context.packageName,
R.layout.monitor_notification_dual_pods_small
R.layout.monitor_notification_unknown_device_small
).apply {
setTextViewText(R.id.device, device.getLabel(context))
}
fun createBigContentView(device: PodDevice): RemoteViews = when (device) {
is DualPodDevice -> createDualPodsBig(device)
is SinglePodDevice -> createSinglePodBig(device)
else -> createUnknownDeviceBig(device)
}
private fun createDualPodsBig(device: DualPodDevice): RemoteViews = RemoteViews(
context.packageName,
R.layout.monitor_notification_dual_pods_big
).apply {
// Left
val leftPercent = device.batteryLeftPodPercent
setImageViewResource(R.id.pod_left_icon, device.leftPodIcon)
setProgressBar(R.id.pod_left_progress, 100, percentToInt(leftPercent), false)
setTextViewText(R.id.pod_left_label, formatBatteryPercent(context, leftPercent))
val isLeftPodCharging = (device as? HasChargeDetectionDual)?.isLeftPodCharging ?: false
setViewVisibility(R.id.pod_left_charging, if (isLeftPodCharging) View.VISIBLE else View.GONE)
val isLeftPodInEar = (device as? HasEarDetectionDual)?.isLeftPodInEar ?: false
setViewVisibility(R.id.pod_left_ear, if (isLeftPodInEar) View.VISIBLE else View.GONE)
// Case
setViewVisibility(R.id.pod_case_container, if (device is HasCase) View.VISIBLE else View.GONE)
(device as? HasCase)?.let { case ->
setImageViewResource(R.id.pod_case_icon, device.caseIcon)
val casePercent = case.batteryCasePercent
setProgressBar(R.id.pod_case_progress, 100, percentToInt(casePercent), false)
setTextViewText(R.id.pod_case_label, formatBatteryPercent(context, casePercent))
setViewVisibility(R.id.pod_case_charging, if (case.isCaseCharging) View.VISIBLE else View.GONE)
}
// Right
val rightPercent = device.batteryRightPodPercent
setImageViewResource(R.id.pod_right_icon, device.rightPodIcon)
setProgressBar(R.id.pod_right_progress, 100, percentToInt(rightPercent), false)
setTextViewText(R.id.pod_right_label, formatBatteryPercent(context, rightPercent))
val isRightPodCharging = (device as? HasChargeDetectionDual)?.isRightPodCharging ?: false
setViewVisibility(R.id.pod_right_charging, if (isRightPodCharging) View.VISIBLE else View.GONE)
val isRightPodInEar = (device as? HasEarDetectionDual)?.isRightPodInEar ?: false
setViewVisibility(R.id.pod_right_ear, if (isRightPodInEar) View.VISIBLE else View.GONE)
}
private fun createSinglePodBig(device: SinglePodDevice): RemoteViews = RemoteViews(
context.packageName,
R.layout.monitor_notification_single_pods_big
).apply {
val headsetPercent = device.batteryHeadsetPercent
setTextViewText(R.id.headphones_label, device.getLabel(context))
setImageViewResource(R.id.headphones_icon, device.iconRes)
setProgressBar(R.id.headphones_battery_progress, 100, percentToInt(headsetPercent), false)
setTextViewText(R.id.headphones_battery_label, formatBatteryPercent(context, headsetPercent))
if (device is HasEarDetection) {
setViewVisibility(R.id.headphones_worn, if (device.isBeingWorn) View.VISIBLE else View.GONE)
}
if (device is HasChargeDetectionDual) {
setViewVisibility(R.id.headphones_charging, if (device.isHeadsetBeingCharged) View.VISIBLE else View.GONE)
}
}
private fun createUnknownDeviceBig(device: PodDevice): RemoteViews = RemoteViews(
context.packageName,
R.layout.monitor_notification_unknown_device_big
).apply {
setTextViewText(R.id.device, device.getLabel(context))
}
private fun percentToInt(percent: Float?): Int {
if (percent == null) return 0
return (percent * 100).roundToInt().coerceIn(0, 100)
}
}
@@ -128,7 +128,8 @@ class MonitorNotifications @Inject constructor(
}
setStyle(NotificationCompat.DecoratedCustomViewStyle())
setCustomBigContentView(notificationViewFactory.createContentView(device))
setCustomContentView(notificationViewFactory.createContentView(device))
setCustomBigContentView(notificationViewFactory.createBigContentView(device))
setContentTitle("$batteryText ~ $stateText")
setSubText(null)
log(TAG, VERBOSE) { "updatingNotification(): $device" }
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<solid android:color="#26808080" />
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<solid android:color="?android:attr/colorAccent" />
</shape>
</clip>
</item>
</layer-list>
@@ -0,0 +1,198 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Notification.Container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Left pod row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="4dp">
<ImageView
android:id="@+id/pod_left_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:tintMode="multiply"
android:tint="?android:attr/colorAccent"
android:src="@drawable/device_airpods_gen1_left" />
<ProgressBar
android:id="@+id/pod_left_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="8dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:max="100"
android:progressDrawable="@drawable/notification_battery_progress"
tools:progress="95" />
<TextView
android:id="@+id/pod_left_label"
style="@style/PodInfoItemText.Notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
tools:text="95%" />
<LinearLayout
android:layout_width="36dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/pod_left_charging"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="2dp"
android:tintMode="multiply"
android:tint="?android:attr/colorAccent"
android:src="@drawable/ic_baseline_power_24" />
<ImageView
android:id="@+id/pod_left_ear"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="2dp"
android:tintMode="multiply"
android:tint="?android:attr/colorAccent"
android:src="@drawable/ic_baseline_hearing_24" />
</LinearLayout>
</LinearLayout>
<!-- Case row -->
<LinearLayout
android:id="@+id/pod_case_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="4dp">
<ImageView
android:id="@+id/pod_case_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:tintMode="multiply"
android:tint="?android:attr/colorAccent"
android:src="@drawable/device_airpods_gen1_case" />
<ProgressBar
android:id="@+id/pod_case_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="8dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:max="100"
android:progressDrawable="@drawable/notification_battery_progress"
tools:progress="88" />
<TextView
android:id="@+id/pod_case_label"
style="@style/PodInfoItemText.Notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
tools:text="88%" />
<LinearLayout
android:layout_width="36dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/pod_case_charging"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="2dp"
android:tintMode="multiply"
android:tint="?android:attr/colorAccent"
android:src="@drawable/ic_baseline_power_24" />
</LinearLayout>
</LinearLayout>
<!-- Right pod row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="4dp">
<ImageView
android:id="@+id/pod_right_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:tintMode="multiply"
android:tint="?android:attr/colorAccent"
android:src="@drawable/device_airpods_gen1_right" />
<ProgressBar
android:id="@+id/pod_right_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="8dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:max="100"
android:progressDrawable="@drawable/notification_battery_progress"
tools:progress="92" />
<TextView
android:id="@+id/pod_right_label"
style="@style/PodInfoItemText.Notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
tools:text="92%" />
<LinearLayout
android:layout_width="36dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/pod_right_charging"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="2dp"
android:tintMode="multiply"
android:tint="?android:attr/colorAccent"
android:src="@drawable/ic_baseline_power_24" />
<ImageView
android:id="@+id/pod_right_ear"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="2dp"
android:tintMode="multiply"
android:tint="?android:attr/colorAccent"
android:src="@drawable/ic_baseline_hearing_24" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Notification.Container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Device name row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="4dp">
<ImageView
android:id="@+id/headphones_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:tintMode="multiply"
android:tint="?android:attr/colorAccent"
android:src="@drawable/twotone_headphones_24" />
<TextView
android:id="@+id/headphones_label"
style="@style/PodInfoItemText.Notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
tools:text="AirPods Max" />
</LinearLayout>
<!-- Battery row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="4dp"
android:paddingStart="32dp"
android:paddingEnd="0dp">
<ProgressBar
android:id="@+id/headphones_battery_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="8dp"
android:layout_weight="1"
android:max="100"
android:progressDrawable="@drawable/notification_battery_progress"
tools:progress="95" />
<TextView
android:id="@+id/headphones_battery_label"
style="@style/PodInfoItemText.Notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
tools:text="95%" />
<ImageView
android:id="@+id/headphones_charging"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="2dp"
android:tintMode="multiply"
android:tint="?android:attr/colorAccent"
android:src="@drawable/ic_baseline_power_24" />
<ImageView
android:id="@+id/headphones_worn"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="2dp"
android:tintMode="multiply"
android:tint="?android:attr/colorAccent"
android:src="@drawable/ic_baseline_hearing_24" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Notification.Container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp">
<TextView
android:id="@+id/device"
style="@style/PodInfoItemText.Notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:drawablePadding="4dp"
tools:text="Unknown Device"
app:drawableStartCompat="@drawable/ic_baseline_question_mark_24" />
</FrameLayout>