Skip to content

Commit 76ede5a

Browse files
committed
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
1 parent 4998a68 commit 76ede5a

File tree

2 files changed

+86
-0
lines changed

2 files changed

+86
-0
lines changed

window/window/src/androidTest/java/androidx/window/layout/SidecarCompatDeviceTest.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919
package androidx.window.layout
2020

2121
import android.content.Context
22+
import android.content.pm.ActivityInfo
23+
import androidx.test.core.app.ActivityScenario
2224
import androidx.test.core.app.ApplicationProvider
2325
import androidx.test.ext.junit.runners.AndroidJUnit4
2426
import androidx.test.filters.LargeTest
27+
import androidx.window.TestConfigChangeHandlingActivity
2528
import androidx.window.WindowTestBase
2629
import androidx.window.core.Version
2730
import androidx.window.layout.ExtensionInterfaceCompat.ExtensionCallbackInterface
@@ -34,6 +37,9 @@ import com.nhaarman.mockitokotlin2.argThat
3437
import com.nhaarman.mockitokotlin2.atLeastOnce
3538
import com.nhaarman.mockitokotlin2.mock
3639
import com.nhaarman.mockitokotlin2.verify
40+
import kotlinx.coroutines.ExperimentalCoroutinesApi
41+
import kotlinx.coroutines.test.TestCoroutineScope
42+
import kotlinx.coroutines.test.runBlockingTest
3743
import org.junit.Assert.assertNotNull
3844
import org.junit.Assume.assumeTrue
3945
import org.junit.Before
@@ -47,6 +53,7 @@ import org.mockito.ArgumentMatcher
4753
*/
4854
@LargeTest
4955
@RunWith(AndroidJUnit4::class)
56+
@OptIn(ExperimentalCoroutinesApi::class)
5057
public class SidecarCompatDeviceTest : WindowTestBase(), CompatDeviceTestInterface {
5158

5259
private lateinit var sidecarCompat: SidecarCompat
@@ -73,6 +80,49 @@ public class SidecarCompatDeviceTest : WindowTestBase(), CompatDeviceTestInterfa
7380
}
7481
}
7582

83+
@Test
84+
fun testWindowLayoutCallbackOnConfigChange() {
85+
val testScope = TestCoroutineScope()
86+
testScope.runBlockingTest {
87+
val scenario = ActivityScenario.launch(TestConfigChangeHandlingActivity::class.java)
88+
val callbackInterface = mock<ExtensionCallbackInterface>()
89+
scenario.onActivity { activity ->
90+
val windowToken = getActivityWindowToken(activity)
91+
assertNotNull(windowToken)
92+
sidecarCompat.setExtensionCallback(callbackInterface)
93+
sidecarCompat.onWindowLayoutChangeListenerAdded(activity)
94+
activity.resetLayoutCounter()
95+
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
96+
activity.waitForLayout()
97+
}
98+
scenario.onActivity { activity ->
99+
val windowToken = getActivityWindowToken(activity)
100+
assertNotNull(windowToken)
101+
val sidecarWindowLayoutInfo =
102+
sidecarCompat.sidecar!!.getWindowLayoutInfo(windowToken)
103+
verify(callbackInterface, atLeastOnce()).onWindowLayoutChanged(
104+
any(),
105+
argThat(SidecarMatcher(sidecarWindowLayoutInfo))
106+
)
107+
}
108+
scenario.onActivity { activity ->
109+
activity.resetLayoutCounter()
110+
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
111+
activity.waitForLayout()
112+
}
113+
scenario.onActivity { activity ->
114+
val windowToken = getActivityWindowToken(activity)
115+
assertNotNull(windowToken)
116+
val updatedSidecarWindowLayoutInfo =
117+
sidecarCompat.sidecar!!.getWindowLayoutInfo(windowToken)
118+
verify(callbackInterface, atLeastOnce()).onWindowLayoutChanged(
119+
any(),
120+
argThat(SidecarMatcher(updatedSidecarWindowLayoutInfo))
121+
)
122+
}
123+
}
124+
}
125+
76126
private fun assumeExtensionV01() {
77127
val sidecarVersion = SidecarCompat.sidecarVersion
78128
assumeTrue(Version.VERSION_0_1 == sidecarVersion)

window/window/src/main/java/androidx/window/layout/SidecarCompat.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ package androidx.window.layout
2020

2121
import android.annotation.SuppressLint
2222
import android.app.Activity
23+
import android.content.ComponentCallbacks
2324
import android.content.Context
25+
import android.content.res.Configuration
2426
import android.os.IBinder
2527
import android.text.TextUtils
2628
import android.util.Log
@@ -51,6 +53,9 @@ internal class SidecarCompat @VisibleForTesting constructor(
5153
// Map of active listeners registered with #onWindowLayoutChangeListenerAdded() and not yet
5254
// removed by #onWindowLayoutChangeListenerRemoved().
5355
private val windowListenerRegisteredContexts = mutableMapOf<IBinder, Activity>()
56+
// Map of activities registered to their component callbacks so we can keep track and
57+
// remove when the activity is unregistered
58+
private val componentCallbackMap = mutableMapOf<Activity, ComponentCallbacks>()
5459
private var extensionCallback: ExtensionCallbackInterface? = null
5560

5661
constructor(context: Context) : this(
@@ -103,18 +108,49 @@ internal class SidecarCompat @VisibleForTesting constructor(
103108
sidecar?.onDeviceStateListenersChanged(false)
104109
}
105110
extensionCallback?.onWindowLayoutChanged(activity, getWindowLayoutInfo(activity))
111+
registerConfigurationChangeListener(activity)
112+
}
113+
114+
private fun registerConfigurationChangeListener(activity: Activity) {
115+
// Only register a component callback if we haven't already as register
116+
// may be called multiple times for the same activity
117+
if (componentCallbackMap[activity] == null) {
118+
// Create a configuration change observer to send updated WindowLayoutInfo
119+
// when the configuration of the app changes: b/186647126
120+
val configChangeObserver = object : ComponentCallbacks {
121+
override fun onConfigurationChanged(newConfig: Configuration) {
122+
extensionCallback?.onWindowLayoutChanged(
123+
activity,
124+
getWindowLayoutInfo(activity)
125+
)
126+
}
127+
128+
override fun onLowMemory() {
129+
return
130+
}
131+
}
132+
componentCallbackMap[activity] = configChangeObserver
133+
activity.registerComponentCallbacks(configChangeObserver)
134+
}
106135
}
107136

108137
override fun onWindowLayoutChangeListenerRemoved(activity: Activity) {
109138
val windowToken = getActivityWindowToken(activity) ?: return
110139
sidecar?.onWindowLayoutChangeListenerRemoved(windowToken)
140+
unregisterComponentCallback(activity)
111141
val isLast = windowListenerRegisteredContexts.size == 1
112142
windowListenerRegisteredContexts.remove(windowToken)
113143
if (isLast) {
114144
sidecar?.onDeviceStateListenersChanged(true)
115145
}
116146
}
117147

148+
private fun unregisterComponentCallback(activity: Activity) {
149+
val configChangeObserver = componentCallbackMap[activity]
150+
activity.unregisterComponentCallbacks(configChangeObserver)
151+
componentCallbackMap.remove(activity)
152+
}
153+
118154
@SuppressLint("BanUncheckedReflection")
119155
override fun validateExtensionInterface(): Boolean {
120156
return try {

0 commit comments

Comments
 (0)