Skip to content

Commit 9c8233d

Browse files
kford55Gerrit Code Review
authored andcommitted
Merge "Supports sending window layout info for activities that handle config changes" into androidx-main
2 parents 4998a68 + 76ede5a commit 9c8233d

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)