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