diff --git a/README.md b/README.md
index 81994c3a..b907591b 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,7 @@ Currently supported models:
## Screenshots



-
+
## Thanks to
* The [OpenPods](https://github.com/adolfintel/OpenPods) project,
diff --git a/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt b/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt
index 316a7df5..54963047 100644
--- a/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt
+++ b/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt
@@ -176,6 +176,15 @@ class PodMonitor @Inject constructor(
}
}
+ suspend fun latestMainDevice(): PodDevice? {
+ val currentMain = mainDevice.firstOrNull()
+ log(TAG) { "Live mainDevice is $currentMain" }
+
+ return currentMain ?: podDeviceCache.loadMainDevice()
+ ?.let { podFactory.createPod(it)?.device }
+ .also { log(TAG) { "Cached mainDevice is $it" } }
+ }
+
private fun getUnfilteredFilter(): ScanFilter {
return ScanFilter.Builder().build()
}
diff --git a/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewFragmentVM.kt b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewFragmentVM.kt
index 03143c18..b13589ee 100644
--- a/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewFragmentVM.kt
+++ b/app-wear/src/main/java/eu/darken/capod/wear/ui/overview/OverviewFragmentVM.kt
@@ -77,7 +77,7 @@ class OverviewFragmentVM @Inject constructor(
debugSettings.isDebugModeEnabled.flow,
bluetoothManager.isBluetoothEnabled,
podMonitor.mainDevice,
- ) { _, permissions, isDebugMode, isBluetoothEnabled, mainPod ->
+ ) { _, permissions, isDebugMode, isBluetoothEnabled, _ ->
val items = mutableListOf()
if (permissions.isNotEmpty()) {
@@ -97,10 +97,7 @@ class OverviewFragmentVM @Inject constructor(
return@combine items
}
- val podToShow = mainPod ?: podDeviceCache.loadMainDevice()?.let {
- log(TAG, VERBOSE) { "Using podDeviceCache: $it" }
- podFactory.createPod(it)?.device
- }
+ val podToShow = podMonitor.latestMainDevice()
log(TAG, VERBOSE) { "Showing $podToShow" }
val now = Instant.now()
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f6862a0f..90e682fd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -69,6 +69,18 @@
+
+
+
+
+
+
+
+
Unit
+ ) {
+ val start = System.currentTimeMillis()
+ asyncBarrier = goAsync()
+ log(TAG, VERBOSE) { "executeAsync($tag) starting asyncBarrier=$asyncBarrier " }
+
+ appScope.launch {
+ try {
+ withTimeout(timeout.toMillis()) { block() }
+ } catch (e: Exception) {
+ log(TAG, ERROR) { "executeAsync($tag) failed: ${e.asLog()}" }
+ } finally {
+ asyncBarrier?.finish()
+ val stop = System.currentTimeMillis()
+ log(TAG, VERBOSE) { "executeAsync($tag) DONE (${stop - start}ms) " }
+ }
+ }
+
+ log(TAG, VERBOSE) { "executeAsync($block) leaving" }
+ }
+
+ override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
+ log(TAG) { "onUpdate(appWidgetIds=${appWidgetIds.toList()})" }
+ executeAsync("onUpdate") {
+ appWidgetIds.forEach { appWidgetId ->
+ updateWidget(context, appWidgetManager, appWidgetId, null)
+ }
+ }
+ }
+
+ override fun onAppWidgetOptionsChanged(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetId: Int,
+ newOptions: Bundle?
+ ) {
+ log(TAG) { "onAppWidgetOptionsChanged(appWidgetId=$appWidgetId, newOptions=$newOptions)" }
+ executeAsync("onAppWidgetOptionsChanged") {
+ updateWidget(context, appWidgetManager, appWidgetId, newOptions)
+ }
+ }
+
+ private suspend fun updateWidget(
+ context: Context,
+ widgetManager: AppWidgetManager,
+ widgetId: Int,
+ options: Bundle?
+ ) {
+ log(TAG) { "updateWidget(widgetId=$widgetId, options=$options)" }
+
+ val device: PodDevice? = podMonitor.latestMainDevice()
+
+ val layout = when {
+ !upgradeRepo.isPro() -> createUpgradeRequiredLayout(context)
+ device is DualPodDevice -> createDualPodLayout(context, device)
+ device is SinglePodDevice -> createSinglePodLayout(context, device)
+ device is PodDevice -> createUnknownPodLayout(context, device)
+ else -> createNoDeviceLayout(context)
+ }
+ widgetManager.updateAppWidget(widgetId, layout)
+ }
+
+ private suspend fun createUpgradeRequiredLayout(
+ context: Context
+ ) = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
+ log(TAG, VERBOSE) { "createUpgradeRequiredLayout(context=$context)" }
+ val pendingIntent: PendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(context, MainActivity::class.java),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ setOnClickPendingIntent(R.id.widget_root, pendingIntent)
+
+ setTextViewText(R.id.primary, context.getString(R.string.upgrade_capod_label))
+ setTextViewText(R.id.secondary, context.getString(R.string.upgrade_capod_description))
+ setViewVisibility(R.id.secondary, View.VISIBLE)
+ }
+
+ private fun createUnknownPodLayout(
+ context: Context,
+ podDevice: PodDevice,
+ ): RemoteViews = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
+ log(TAG, VERBOSE) { "createUnknownPodLayout(context=$context, podDevice=$podDevice)" }
+ val pendingIntent: PendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(context, MainActivity::class.java),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ setOnClickPendingIntent(R.id.widget_root, pendingIntent)
+
+ setTextViewText(R.id.primary, context.getString(R.string.pods_unknown_label))
+ }
+
+ private fun createNoDeviceLayout(
+ context: Context,
+ ): RemoteViews = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
+ log(TAG, VERBOSE) { "createNoDeviceLayout(context=$context)" }
+ val pendingIntent: PendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(context, MainActivity::class.java),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ setOnClickPendingIntent(R.id.widget_root, pendingIntent)
+
+ setTextViewText(R.id.primary, context.getString(R.string.overview_nomaindevice_label))
+ }
+
+ private fun createDualPodLayout(
+ context: Context,
+ podDevice: DualPodDevice,
+ ): RemoteViews = RemoteViews(context.packageName, R.layout.widget_pod_dual_layout).apply {
+ log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice)" }
+ val pendingIntent: PendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(context, MainActivity::class.java),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ setOnClickPendingIntent(R.id.widget_root, pendingIntent)
+
+ setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
+
+ // Left
+ setTextViewText(R.id.pod_left_label, podDevice.getBatteryLevelLeftPod(context))
+ setViewVisibility(
+ R.id.pod_left_charging,
+ if (podDevice is HasChargeDetectionDual && podDevice.isLeftPodCharging) View.VISIBLE else View.GONE
+ )
+ setViewVisibility(
+ R.id.pod_left_ear,
+ if (podDevice is HasEarDetectionDual && podDevice.isLeftPodInEar) View.VISIBLE else View.GONE
+ )
+
+ // Case
+ setTextViewText(R.id.pod_case_label, (podDevice as? HasCase)?.getBatteryLevelCase(context))
+ setViewVisibility(
+ R.id.pod_case_charging,
+ if (podDevice is HasCase && podDevice.isCaseCharging) View.VISIBLE else View.GONE
+ )
+
+ // Right
+ setTextViewText(R.id.pod_right_label, podDevice.getBatteryLevelRightPod(context))
+ setViewVisibility(
+ R.id.pod_right_charging,
+ if (podDevice is HasChargeDetectionDual && podDevice.isRightPodCharging) View.VISIBLE else View.GONE
+ )
+ setViewVisibility(
+ R.id.pod_right_ear,
+ if (podDevice is HasEarDetectionDual && podDevice.isRightPodInEar) View.VISIBLE else View.GONE
+ )
+ }
+
+ private fun createSinglePodLayout(
+ context: Context,
+ podDevice: SinglePodDevice,
+ ): RemoteViews = RemoteViews(context.packageName, R.layout.widget_pod_single_layout).apply {
+ log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice)" }
+ val pendingIntent: PendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(context, MainActivity::class.java),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ setOnClickPendingIntent(R.id.widget_root, pendingIntent)
+
+ setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
+ setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(podDevice.batteryHeadsetPercent))
+ setTextViewText(R.id.headphones_battery_label, podDevice.getBatteryLevelHeadset(context))
+
+ setViewVisibility(
+ R.id.headphones_worn,
+ if (podDevice is HasEarDetection && podDevice.isBeingWorn) View.VISIBLE else View.GONE
+ )
+
+ if (this is HasChargeDetectionDual) {
+ setViewVisibility(R.id.headphones_charging, if (isHeadsetBeingCharged) View.VISIBLE else View.GONE)
+ }
+ }
+
+ companion object {
+ val TAG = logTag("Widget", "Provider")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt
index f4804aa4..4f22cec9 100644
--- a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt
+++ b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt
@@ -20,6 +20,7 @@ import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.main.core.MonitorMode
import eu.darken.capod.main.core.PermissionTool
+import eu.darken.capod.main.ui.widget.WidgetManager
import eu.darken.capod.monitor.core.MonitorComponent
import eu.darken.capod.monitor.core.MonitorCoroutineScope
import eu.darken.capod.monitor.core.PodMonitor
@@ -48,6 +49,7 @@ class MonitorWorker @AssistedInject constructor(
private val autoConnect: AutoConnect,
private val popUpReaction: PopUpReaction,
private val popUpWindow: PopUpWindow,
+ private val widgetManager: WidgetManager,
) : CoroutineWorker(context, params) {
private val workerScope = MonitorCoroutineScope()
@@ -127,10 +129,10 @@ class MonitorWorker @AssistedInject constructor(
.flatMapLatest { (monitorMode, devices) ->
log(TAG) { "Monitor mode: $monitorMode" }
when (monitorMode) {
- MonitorMode.MANUAL -> flow {
- // Cancel worker, ui scans manually
- workerScope.coroutineContext.cancelChildren()
- }
+ MonitorMode.MANUAL -> flow {
+ // Cancel worker, ui scans manually
+ workerScope.coroutineContext.cancelChildren()
+ }
MonitorMode.ALWAYS -> emptyFlow()
MonitorMode.AUTOMATIC -> flow {
val mainAddress = generalSettings.mainDeviceAddress.value
diff --git a/app/src/main/res/layout/widget_loading_layout.xml b/app/src/main/res/layout/widget_loading_layout.xml
new file mode 100644
index 00000000..82834f80
--- /dev/null
+++ b/app/src/main/res/layout/widget_loading_layout.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_message_layout.xml b/app/src/main/res/layout/widget_message_layout.xml
new file mode 100644
index 00000000..68723f1d
--- /dev/null
+++ b/app/src/main/res/layout/widget_message_layout.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_pod_dual_layout.xml b/app/src/main/res/layout/widget_pod_dual_layout.xml
new file mode 100644
index 00000000..adf90920
--- /dev/null
+++ b/app/src/main/res/layout/widget_pod_dual_layout.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_pod_single_layout.xml b/app/src/main/res/layout/widget_pod_single_layout.xml
new file mode 100644
index 00000000..12d7eec2
--- /dev/null
+++ b/app/src/main/res/layout/widget_pod_single_layout.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_preview_layout.xml b/app/src/main/res/layout/widget_preview_layout.xml
new file mode 100644
index 00000000..f24b5219
--- /dev/null
+++ b/app/src/main/res/layout/widget_preview_layout.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
new file mode 100644
index 00000000..5b64bdb7
--- /dev/null
+++ b/app/src/main/res/values/ids.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 11572aed..1a2566f0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -89,4 +89,6 @@
Translators
darken
+
+ A widget showing the last known device status.
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 44313c5c..ebd3559f 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -26,4 +26,29 @@
- true
- end
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/battery_widget_info.xml b/app/src/main/res/xml/battery_widget_info.xml
new file mode 100644
index 00000000..ace71875
--- /dev/null
+++ b/app/src/main/res/xml/battery_widget_info.xml
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
new file mode 100644
index 00000000..c6d95ab1
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ