From 76ede5aa71644616f765fb9786542db668e71fc1 Mon Sep 17 00:00:00 2001 From: Kenneth Ford Date: Fri, 9 Jul 2021 22:55:40 +0000 Subject: [PATCH] Supports sending window layout info for activities that handle config changes Fixes bug where activities that handle configuration changes were not getting the updated window layout info when these would occur Bug: 186647126 Test: SidecarCompatDeviceTest & Manual testing Change-Id: I636129f1f5c2bf27909efe787fe88820c30f7509 --- .../window/layout/SidecarCompatDeviceTest.kt | 50 +++++++++++++++++++ .../androidx/window/layout/SidecarCompat.kt | 36 +++++++++++++ 2 files changed, 86 insertions(+) diff --git a/window/window/src/androidTest/java/androidx/window/layout/SidecarCompatDeviceTest.kt b/window/window/src/androidTest/java/androidx/window/layout/SidecarCompatDeviceTest.kt index 0d97b77588e4c..d38b1317365e7 100644 --- a/window/window/src/androidTest/java/androidx/window/layout/SidecarCompatDeviceTest.kt +++ b/window/window/src/androidTest/java/androidx/window/layout/SidecarCompatDeviceTest.kt @@ -19,9 +19,12 @@ package androidx.window.layout import android.content.Context +import android.content.pm.ActivityInfo +import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import androidx.window.TestConfigChangeHandlingActivity import androidx.window.WindowTestBase import androidx.window.core.Version import androidx.window.layout.ExtensionInterfaceCompat.ExtensionCallbackInterface @@ -34,6 +37,9 @@ import com.nhaarman.mockitokotlin2.argThat import com.nhaarman.mockitokotlin2.atLeastOnce import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.runBlockingTest import org.junit.Assert.assertNotNull import org.junit.Assume.assumeTrue import org.junit.Before @@ -47,6 +53,7 @@ import org.mockito.ArgumentMatcher */ @LargeTest @RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) public class SidecarCompatDeviceTest : WindowTestBase(), CompatDeviceTestInterface { private lateinit var sidecarCompat: SidecarCompat @@ -73,6 +80,49 @@ public class SidecarCompatDeviceTest : WindowTestBase(), CompatDeviceTestInterfa } } + @Test + fun testWindowLayoutCallbackOnConfigChange() { + val testScope = TestCoroutineScope() + testScope.runBlockingTest { + val scenario = ActivityScenario.launch(TestConfigChangeHandlingActivity::class.java) + val callbackInterface = mock() + scenario.onActivity { activity -> + val windowToken = getActivityWindowToken(activity) + assertNotNull(windowToken) + sidecarCompat.setExtensionCallback(callbackInterface) + sidecarCompat.onWindowLayoutChangeListenerAdded(activity) + activity.resetLayoutCounter() + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + activity.waitForLayout() + } + scenario.onActivity { activity -> + val windowToken = getActivityWindowToken(activity) + assertNotNull(windowToken) + val sidecarWindowLayoutInfo = + sidecarCompat.sidecar!!.getWindowLayoutInfo(windowToken) + verify(callbackInterface, atLeastOnce()).onWindowLayoutChanged( + any(), + argThat(SidecarMatcher(sidecarWindowLayoutInfo)) + ) + } + scenario.onActivity { activity -> + activity.resetLayoutCounter() + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + activity.waitForLayout() + } + scenario.onActivity { activity -> + val windowToken = getActivityWindowToken(activity) + assertNotNull(windowToken) + val updatedSidecarWindowLayoutInfo = + sidecarCompat.sidecar!!.getWindowLayoutInfo(windowToken) + verify(callbackInterface, atLeastOnce()).onWindowLayoutChanged( + any(), + argThat(SidecarMatcher(updatedSidecarWindowLayoutInfo)) + ) + } + } + } + private fun assumeExtensionV01() { val sidecarVersion = SidecarCompat.sidecarVersion assumeTrue(Version.VERSION_0_1 == sidecarVersion) diff --git a/window/window/src/main/java/androidx/window/layout/SidecarCompat.kt b/window/window/src/main/java/androidx/window/layout/SidecarCompat.kt index 6c97b750a8e36..4f606972f9ca3 100644 --- a/window/window/src/main/java/androidx/window/layout/SidecarCompat.kt +++ b/window/window/src/main/java/androidx/window/layout/SidecarCompat.kt @@ -20,7 +20,9 @@ package androidx.window.layout import android.annotation.SuppressLint import android.app.Activity +import android.content.ComponentCallbacks import android.content.Context +import android.content.res.Configuration import android.os.IBinder import android.text.TextUtils import android.util.Log @@ -51,6 +53,9 @@ internal class SidecarCompat @VisibleForTesting constructor( // Map of active listeners registered with #onWindowLayoutChangeListenerAdded() and not yet // removed by #onWindowLayoutChangeListenerRemoved(). private val windowListenerRegisteredContexts = mutableMapOf() + // Map of activities registered to their component callbacks so we can keep track and + // remove when the activity is unregistered + private val componentCallbackMap = mutableMapOf() private var extensionCallback: ExtensionCallbackInterface? = null constructor(context: Context) : this( @@ -103,11 +108,36 @@ internal class SidecarCompat @VisibleForTesting constructor( sidecar?.onDeviceStateListenersChanged(false) } extensionCallback?.onWindowLayoutChanged(activity, getWindowLayoutInfo(activity)) + registerConfigurationChangeListener(activity) + } + + private fun registerConfigurationChangeListener(activity: Activity) { + // Only register a component callback if we haven't already as register + // may be called multiple times for the same activity + if (componentCallbackMap[activity] == null) { + // Create a configuration change observer to send updated WindowLayoutInfo + // when the configuration of the app changes: b/186647126 + val configChangeObserver = object : ComponentCallbacks { + override fun onConfigurationChanged(newConfig: Configuration) { + extensionCallback?.onWindowLayoutChanged( + activity, + getWindowLayoutInfo(activity) + ) + } + + override fun onLowMemory() { + return + } + } + componentCallbackMap[activity] = configChangeObserver + activity.registerComponentCallbacks(configChangeObserver) + } } override fun onWindowLayoutChangeListenerRemoved(activity: Activity) { val windowToken = getActivityWindowToken(activity) ?: return sidecar?.onWindowLayoutChangeListenerRemoved(windowToken) + unregisterComponentCallback(activity) val isLast = windowListenerRegisteredContexts.size == 1 windowListenerRegisteredContexts.remove(windowToken) if (isLast) { @@ -115,6 +145,12 @@ internal class SidecarCompat @VisibleForTesting constructor( } } + private fun unregisterComponentCallback(activity: Activity) { + val configChangeObserver = componentCallbackMap[activity] + activity.unregisterComponentCallbacks(configChangeObserver) + componentCallbackMap.remove(activity) + } + @SuppressLint("BanUncheckedReflection") override fun validateExtensionInterface(): Boolean { return try {