Optimization/fix to isBatteryLowAndNotCharging

This patch moves the initial setup of isBatteryLowAndNotCharging
earlier which means it can be done in parallel with
createWatchFace. In addition we now listen to ACTION_POWER_DISCONNECTED

Test: Manual testing
Change-Id: I0a2660fffc90a798b6c7c7949714a8281b8a520b
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsObserver.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsObserver.kt
index eead284..34e1442 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsObserver.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsObserver.kt
@@ -31,6 +31,8 @@
     private val deferredWatchFaceImpl: Deferred<WatchFaceImpl>,
     private val uiThreadCoroutineScope: CoroutineScope
 ) : BroadcastsReceiver.BroadcastEventObserver {
+    private var batteryLow: Boolean? = null
+    private var charging: Boolean? = null
 
     override fun onActionTimeTick() {
         if (!watchState.isAmbient.value!!) {
@@ -51,17 +53,29 @@
     }
 
     override fun onActionBatteryLow() {
-        updateBatteryLowAndNotChargingStatus(true)
+        batteryLow = true
+        if (charging == false) {
+            updateBatteryLowAndNotChargingStatus(true)
+        }
     }
 
     override fun onActionBatteryOkay() {
+        batteryLow = false
         updateBatteryLowAndNotChargingStatus(false)
     }
 
     override fun onActionPowerConnected() {
+        charging = true
         updateBatteryLowAndNotChargingStatus(false)
     }
 
+    override fun onActionPowerDisconnected() {
+        charging = false
+        if (batteryLow == true) {
+            updateBatteryLowAndNotChargingStatus(true)
+        }
+    }
+
     override fun onMockTime(intent: Intent) {
         uiThreadCoroutineScope.launch {
             deferredWatchFaceImpl.await().onMockTime(intent)
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt
index 38d60a5..6411050 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt
@@ -20,8 +20,10 @@
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
+import android.os.BatteryManager
 import androidx.annotation.RestrictTo
 import androidx.annotation.UiThread
+import androidx.wear.watchface.BroadcastsReceiver.BroadcastEventObserver
 
 /**
  * This class decouples [BroadcastEventObserver]s from the actual broadcast event receivers to make
@@ -59,11 +61,23 @@
         @UiThread
         public fun onActionPowerConnected()
 
+        /** Called when we receive [Intent.ACTION_POWER_DISCONNECTED]. */
+        @UiThread
+        public fun onActionPowerDisconnected()
+
         /** Called when we receive [WatchFaceImpl.MOCK_TIME_INTENT]. */
         @UiThread
         public fun onMockTime(intent: Intent)
     }
 
+    companion object {
+        // The threshold used to judge whether the battery is low during initialization.  Ideally
+        // we would use the threshold for Intent.ACTION_BATTERY_LOW but it's not documented or
+        // available programmatically. The value below is the default but it could be overridden
+        // by OEMs.
+        internal const val INITIAL_LOW_BATTERY_THRESHOLD = 15f
+    }
+
     internal val actionTimeTickReceiver: BroadcastReceiver = object : BroadcastReceiver() {
         @SuppressWarnings("SyntheticAccessor")
         override fun onReceive(context: Context, intent: Intent) {
@@ -104,6 +118,13 @@
         }
     }
 
+    internal val actionPowerDisconnectedReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        @SuppressWarnings("SyntheticAccessor")
+        override fun onReceive(context: Context, intent: Intent) {
+            observer.onActionPowerDisconnected()
+        }
+    }
+
     internal val mockTimeReceiver: BroadcastReceiver = object : BroadcastReceiver() {
         @SuppressWarnings("SyntheticAccessor")
         override fun onReceive(context: Context, intent: Intent) {
@@ -127,9 +148,35 @@
             actionPowerConnectedReceiver,
             IntentFilter(Intent.ACTION_POWER_CONNECTED)
         )
+        context.registerReceiver(
+            actionPowerDisconnectedReceiver,
+            IntentFilter(Intent.ACTION_POWER_DISCONNECTED)
+        )
         context.registerReceiver(mockTimeReceiver, IntentFilter(WatchFaceImpl.MOCK_TIME_INTENT))
     }
 
+    /** Called to send observers initial battery state in advance of receiving any broadcasts. */
+    internal fun processBatteryStatus(batteryStatus: Intent?) {
+        val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
+        if (status == BatteryManager.BATTERY_STATUS_CHARGING ||
+            status == BatteryManager.BATTERY_STATUS_FULL) {
+            observer.onActionPowerConnected()
+        } else {
+            observer.onActionPowerDisconnected()
+        }
+
+        val batteryPercent: Float = batteryStatus?.let { intent ->
+            val level: Int = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
+            val scale: Int = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
+            level * 100 / scale.toFloat()
+        } ?: 100.0f
+        if (batteryPercent < INITIAL_LOW_BATTERY_THRESHOLD) {
+            observer.onActionBatteryLow()
+        } else {
+            observer.onActionBatteryOkay()
+        }
+    }
+
     public fun onDestroy() {
         context.unregisterReceiver(actionTimeTickReceiver)
         context.unregisterReceiver(actionTimeZoneReceiver)
@@ -139,4 +186,4 @@
         context.unregisterReceiver(actionPowerConnectedReceiver)
         context.unregisterReceiver(mockTimeReceiver)
     }
-}
\ No newline at end of file
+}
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index c3b16a6..c8131ae 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -21,11 +21,9 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
-import android.content.IntentFilter
 import android.graphics.Bitmap
 import android.graphics.Canvas
 import android.graphics.Rect
-import android.os.BatteryManager
 import android.os.Build
 import android.os.Bundle
 import android.os.Handler
@@ -57,7 +55,6 @@
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.launch
@@ -417,7 +414,7 @@
     @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     public var complicationSlotsManager: ComplicationSlotsManager,
 
-    private val broadcastsObserver: BroadcastsObserver,
+    internal val broadcastsObserver: BroadcastsObserver,
     internal var broadcastsReceiver: BroadcastsReceiver?
 ) {
     internal companion object {
@@ -446,12 +443,6 @@
         // clamp the framerate to a maximum of 10fps to conserve power.
         internal const val MAX_LOW_POWER_INTERACTIVE_UPDATE_RATE_MS = 100L
 
-        // The threshold used to judge whether the battery is low during initialization.  Ideally
-        // we would use the threshold for Intent.ACTION_BATTERY_LOW but it's not documented or
-        // available programmatically. The value below is the default but it could be overridden
-        // by OEMs.
-        internal const val INITIAL_LOW_BATTERY_THRESHOLD = 15.0f
-
         // Number of milliseconds before the target draw time for the delayed task to run and post a
         // choreographer frame. This is necessary when rendering at less than 60 fps to make sure we
         // post the choreographer frame in time to for us to render in the desired frame.
@@ -616,12 +607,6 @@
             watchFaceHostApi.updateContentDescriptionLabels()
         }
 
-        setIsBatteryLowAndNotChargingFromBatteryStatus(
-            IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { iFilter ->
-                watchFaceHostApi.getContext().registerReceiver(null, iFilter)
-            }
-        )
-
         if (!watchState.isHeadless) {
             WatchFace.registerEditorDelegate(componentName, WFEditorDelegate())
         }
@@ -735,20 +720,6 @@
         }
     }
 
-    internal fun setIsBatteryLowAndNotChargingFromBatteryStatus(batteryStatus: Intent?) {
-        val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
-        val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
-            status == BatteryManager.BATTERY_STATUS_FULL
-        val batteryPercent: Float = batteryStatus?.let { intent ->
-            val level: Int = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
-            val scale: Int = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
-            level * 100 / scale.toFloat()
-        } ?: 100.0f
-        val isBatteryLowAndNotCharging = watchState.isBatteryLowAndNotCharging as MutableStateFlow
-        isBatteryLowAndNotCharging.value =
-            (batteryPercent < INITIAL_LOW_BATTERY_THRESHOLD) && !isCharging
-    }
-
     /** Called by the system in response to remote configuration. */
     @UiThread
     internal fun onSetStyleInternal(style: UserStyle) {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 5be8275..d814da5 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -20,6 +20,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.content.IntentFilter
 import android.graphics.Canvas
 import android.graphics.Rect
 import android.os.Build
@@ -1496,7 +1497,13 @@
                 if (watchState.isHeadless) {
                     null
                 } else {
-                    BroadcastsReceiver(_context, broadcastsObserver)
+                    BroadcastsReceiver(_context, broadcastsObserver).apply {
+                        processBatteryStatus(
+                            IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { iFilter ->
+                                _context.registerReceiver(null, iFilter)
+                            }
+                        )
+                    }
                 }
             }
 
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index b7acfd5..f8a1c6d 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -2432,7 +2432,7 @@
     }
 
     @Test
-    public fun setIsBatteryLowAndNotChargingFromBatteryStatus() {
+    public fun isBatteryLowAndNotCharging_modified_by_broadcasts() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
             emptyList(),
@@ -2451,7 +2451,41 @@
             )
         )
 
-        watchFaceImpl.setIsBatteryLowAndNotChargingFromBatteryStatus(
+        watchFaceImpl.broadcastsObserver.onActionPowerConnected()
+        watchFaceImpl.broadcastsObserver.onActionBatteryOkay()
+        assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
+
+        watchFaceImpl.broadcastsObserver.onActionBatteryLow()
+        assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
+
+        watchFaceImpl.broadcastsObserver.onActionPowerDisconnected()
+        assertTrue(watchState.isBatteryLowAndNotCharging.value!!)
+
+        watchFaceImpl.broadcastsObserver.onActionBatteryOkay()
+        assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
+    }
+
+    @Test
+    public fun processBatteryStatus() {
+        initWallpaperInteractiveWatchFaceInstance(
+            WatchFaceType.ANALOG,
+            emptyList(),
+            UserStyleSchema(emptyList()),
+            WallpaperInteractiveWatchFaceInstanceParams(
+                "interactiveInstanceId",
+                DeviceConfig(
+                    false,
+                    false,
+                    0,
+                    0
+                ),
+                WatchUiState(false, 0),
+                UserStyle(emptyMap()).toWireFormat(),
+                null
+            )
+        )
+
+        watchFaceImpl.broadcastsReceiver!!.processBatteryStatus(
             Intent().apply {
                 putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_DISCHARGING)
                 putExtra(BatteryManager.EXTRA_LEVEL, 0)
@@ -2460,7 +2494,7 @@
         )
         assertTrue(watchState.isBatteryLowAndNotCharging.value!!)
 
-        watchFaceImpl.setIsBatteryLowAndNotChargingFromBatteryStatus(
+        watchFaceImpl.broadcastsReceiver!!.processBatteryStatus(
             Intent().apply {
                 putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING)
                 putExtra(BatteryManager.EXTRA_LEVEL, 0)
@@ -2469,7 +2503,7 @@
         )
         assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
 
-        watchFaceImpl.setIsBatteryLowAndNotChargingFromBatteryStatus(
+        watchFaceImpl.broadcastsReceiver!!.processBatteryStatus(
             Intent().apply {
                 putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_DISCHARGING)
                 putExtra(BatteryManager.EXTRA_LEVEL, 80)
@@ -2478,10 +2512,10 @@
         )
         assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
 
-        watchFaceImpl.setIsBatteryLowAndNotChargingFromBatteryStatus(Intent())
+        watchFaceImpl.broadcastsReceiver!!.processBatteryStatus(Intent())
         assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
 
-        watchFaceImpl.setIsBatteryLowAndNotChargingFromBatteryStatus(null)
+        watchFaceImpl.broadcastsReceiver!!.processBatteryStatus(null)
         assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
     }