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