Fix editor memory leak on Samsung devices

Bug: 262315357
Test: Manually confirmed this fixes the leak on a GW4. Added a test to show the headless instance was disposed.
Change-Id: Ic73476e0db40eec9789b0de9ab87686a3b3a49c4
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 541c0da..6ed0000 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
@@ -44,6 +44,7 @@
 import androidx.wear.watchface.complications.SystemDataSources
 import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.data.toApiComplicationData
+import androidx.wear.watchface.control.HeadlessWatchFaceImpl
 import androidx.wear.watchface.control.data.ComplicationRenderParams
 import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
 import androidx.wear.watchface.control.data.WatchFaceRenderParams
@@ -207,8 +208,8 @@
                 watchFaceService.setContext(context)
                 val engine = watchFaceService.createHeadlessEngine() as
                     WatchFaceService.EngineWrapper
-                engine.createHeadlessInstance(params)
-                return engine.deferredWatchFaceImpl.await().WFEditorDelegate()
+                val headlessWatchFaceImpl = engine.createHeadlessInstance(params)
+                return engine.deferredWatchFaceImpl.await().WFEditorDelegate(headlessWatchFaceImpl)
             }
         }
     }
@@ -727,7 +728,10 @@
         }
 
         if (!watchState.isHeadless) {
-            WatchFace.registerEditorDelegate(componentName, WFEditorDelegate())
+            WatchFace.registerEditorDelegate(
+                componentName,
+                WFEditorDelegate(headlessWatchFaceImpl = null)
+            )
             registerReceivers()
         }
 
@@ -778,7 +782,9 @@
         }
     }
 
-    internal inner class WFEditorDelegate : WatchFace.EditorDelegate {
+    internal inner class WFEditorDelegate(
+        private val headlessWatchFaceImpl: HeadlessWatchFaceImpl?
+    ) : WatchFace.EditorDelegate {
         override val userStyleSchema
             get() = currentUserStyleRepository.schema
 
@@ -849,8 +855,10 @@
             complicationSlotsManager.configExtrasChangeCallback = callback
         }
 
+        @SuppressLint("NewApi") // release
         override fun onDestroy(): Unit = TraceEvent("WFEditorDelegate.onDestroy").use {
             if (watchState.isHeadless) {
+                headlessWatchFaceImpl!!.release()
                 [email protected]()
             }
         }
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
index a5f3897..2c2540b 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
@@ -55,7 +55,7 @@
             indentingPrintWriter.decreaseIndent()
         }
 
-        private val headlessInstances = HashSet()
+        internal val headlessInstances = HashSet()
     }
 
     init {
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 790e749..ed14a86 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
@@ -18,6 +18,7 @@
 
 import android.support.wearable.complications.ComplicationData as WireComplicationData
 import android.support.wearable.complications.ComplicationText as WireComplicationText
+import android.annotation.SuppressLint
 import android.app.NotificationManager
 import android.app.PendingIntent
 import android.content.ComponentName
@@ -67,6 +68,7 @@
 import androidx.wear.watchface.complications.data.TimeDifferenceStyle
 import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
+import androidx.wear.watchface.control.HeadlessWatchFaceImpl
 import androidx.wear.watchface.control.IInteractiveWatchFace
 import androidx.wear.watchface.control.IPendingInteractiveWatchFace
 import androidx.wear.watchface.control.IWatchfaceListener
@@ -6511,6 +6513,42 @@
             .isInstanceOf(ShortTextComplicationData::class.java)
     }
 
+    @SuppressLint("NewApi")
+    @Test
+    public fun createHeadlessSessionDelegate_onDestroy() {
+        val context = ApplicationProvider.getApplicationContext()
+        val componentName = ComponentName(context, TestNopCanvasWatchFaceService::class.java)
+        lateinit var delegate: WatchFace.EditorDelegate
+
+        // Allows us to programmatically control tasks.
+        TestNopCanvasWatchFaceService.handler = this.handler
+
+        CoroutineScope(handler.asCoroutineDispatcher().immediate).launch {
+            delegate = WatchFace.createHeadlessSessionDelegate(
+                componentName,
+                HeadlessWatchFaceInstanceParams(
+                    componentName,
+                    DeviceConfig(false, false, 100, 200),
+                    100,
+                    100,
+                    null
+                ),
+                context
+            )
+        }
+
+        // Run all pending tasks.
+        while (pendingTasks.isNotEmpty()) {
+            pendingTasks.remove().runnable.run()
+        }
+
+        assertThat(HeadlessWatchFaceImpl.headlessInstances).isNotEmpty()
+        delegate.onDestroy()
+
+        // The headlessInstances should become empty, otherwise there's a leak.
+        assertThat(HeadlessWatchFaceImpl.headlessInstances).isEmpty()
+    }
+
     private fun getLeftShortTextComplicationDataText(): CharSequence {
         val complication = complicationSlotsManager[
             LEFT_COMPLICATION_ID
@@ -6542,3 +6580,50 @@
 
     private suspend fun  Deferred.awaitWithTimeout(): T = withTimeout(1000) { await() }
 }
+
+class TestNopCanvasWatchFaceService : WatchFaceService() {
+    companion object {
+        lateinit var handler: Handler
+    }
+
+    override fun getUiThreadHandlerImpl() = handler
+
+    // To make unit tests simpler and non-flaky we run background tasks and ui tasks on the same
+    // handler.
+    override fun getBackgroundThreadHandlerImpl() = handler
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ) = WatchFace(
+        WatchFaceType.DIGITAL,
+        @Suppress("deprecation")
+        object : Renderer.CanvasRenderer(
+            surfaceHolder,
+            currentUserStyleRepository,
+            watchState,
+            CanvasType.HARDWARE,
+            16
+        ) {
+            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
+                // Intentionally empty.
+            }
+
+            override fun renderHighlightLayer(
+                canvas: Canvas,
+                bounds: Rect,
+                zonedDateTime: ZonedDateTime
+            ) {
+                // Intentionally empty.
+            }
+        }
+    )
+
+    override fun getSystemTimeProvider() = object : SystemTimeProvider {
+        override fun getSystemTimeMillis() = 123456789L
+
+        override fun getSystemTimeZoneId() = ZoneId.of("UTC")
+    }
+}
\ No newline at end of file