Add test artifact to support testing

Add test artifact to support testing.
Add EmptyWindowLayoutInfoRule to always produce an empty set of features

Relnote: Add test artifact for WindowManager Jetpack Library.

Bug: 184266682
Test: ./gradlew window:window-test:cAT
Change-Id: I57f66ad83498ff5e09d0a1d1c9cda0554c2c04e1
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 1ffc337..e4a46cc 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -279,6 +279,7 @@
     docs(project(":window:window-rxjava3"))
     stubs(project(":window:window-sidecar"))
     stubs(project(":window:window-extensions"))
+    docs(project(":window:window-testing"))
     docs(project(":work:work-gcm"))
     docs(project(":work:work-multiprocess"))
     docs(project(":work:work-runtime"))
diff --git a/settings.gradle b/settings.gradle
index ba10801..ee9269e 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -622,6 +622,7 @@
 includeProject(":window:window-rxjava3", "window/window-rxjava3", [BuildType.MAIN])
 includeProject(":window:window-samples", "window/window-samples", [BuildType.MAIN])
 includeProject(":window:window-sidecar", "window/window-sidecar", [BuildType.MAIN])
+includeProject(":window:window-testing", "window/window-testing", [BuildType.MAIN])
 includeProject(":work:integration-tests:testapp", "work/integration-tests/testapp", [BuildType.MAIN])
 includeProject(":work:work-benchmark", "work/workmanager-benchmark", [BuildType.MAIN])
 includeProject(":work:work-gcm", "work/workmanager-gcm", [BuildType.MAIN])
diff --git a/window/window-java/api/current.txt b/window/window-java/api/current.txt
index c041785..b4bdb38 100644
--- a/window/window-java/api/current.txt
+++ b/window/window-java/api/current.txt
@@ -4,10 +4,10 @@
   public final class WindowInfoRepoJavaAdapter implements androidx.window.WindowInfoRepo {
     ctor public WindowInfoRepoJavaAdapter(androidx.window.WindowInfoRepo repo);
     method public void addWindowLayoutInfoListener(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> consumer);
-    method public androidx.window.WindowMetrics currentWindowMetrics();
-    method public androidx.window.WindowMetrics maximumWindowMetrics();
+    method public androidx.window.WindowMetrics getCurrentWindowMetrics();
+    method public androidx.window.WindowMetrics getMaximumWindowMetrics();
+    method public kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> getWindowLayoutInfo();
     method public void removeWindowLayoutInfoListener(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> consumer);
-    method public kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> windowLayoutInfo();
   }
 
 }
diff --git a/window/window-java/api/public_plus_experimental_current.txt b/window/window-java/api/public_plus_experimental_current.txt
index c041785..b4bdb38 100644
--- a/window/window-java/api/public_plus_experimental_current.txt
+++ b/window/window-java/api/public_plus_experimental_current.txt
@@ -4,10 +4,10 @@
   public final class WindowInfoRepoJavaAdapter implements androidx.window.WindowInfoRepo {
     ctor public WindowInfoRepoJavaAdapter(androidx.window.WindowInfoRepo repo);
     method public void addWindowLayoutInfoListener(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> consumer);
-    method public androidx.window.WindowMetrics currentWindowMetrics();
-    method public androidx.window.WindowMetrics maximumWindowMetrics();
+    method public androidx.window.WindowMetrics getCurrentWindowMetrics();
+    method public androidx.window.WindowMetrics getMaximumWindowMetrics();
+    method public kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> getWindowLayoutInfo();
     method public void removeWindowLayoutInfoListener(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> consumer);
-    method public kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> windowLayoutInfo();
   }
 
 }
diff --git a/window/window-java/api/restricted_current.txt b/window/window-java/api/restricted_current.txt
index c041785..b4bdb38 100644
--- a/window/window-java/api/restricted_current.txt
+++ b/window/window-java/api/restricted_current.txt
@@ -4,10 +4,10 @@
   public final class WindowInfoRepoJavaAdapter implements androidx.window.WindowInfoRepo {
     ctor public WindowInfoRepoJavaAdapter(androidx.window.WindowInfoRepo repo);
     method public void addWindowLayoutInfoListener(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> consumer);
-    method public androidx.window.WindowMetrics currentWindowMetrics();
-    method public androidx.window.WindowMetrics maximumWindowMetrics();
+    method public androidx.window.WindowMetrics getCurrentWindowMetrics();
+    method public androidx.window.WindowMetrics getMaximumWindowMetrics();
+    method public kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> getWindowLayoutInfo();
     method public void removeWindowLayoutInfoListener(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> consumer);
-    method public kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> windowLayoutInfo();
   }
 
 }
diff --git a/window/window-java/src/androidTest/java/androidx/window/java/WindowInfoRepoJavaAdapterTest.kt b/window/window-java/src/androidTest/java/androidx/window/java/WindowInfoRepoJavaAdapterTest.kt
index 46041e5..688df0e 100644
--- a/window/window-java/src/androidTest/java/androidx/window/java/WindowInfoRepoJavaAdapterTest.kt
+++ b/window/window-java/src/androidTest/java/androidx/window/java/WindowInfoRepoJavaAdapterTest.kt
@@ -42,10 +42,10 @@
     public fun testCurrentWindowMetrics_delegatesToRepo() {
         val expected = WindowMetrics(Rect(0, 1, 2, 3))
         val mockRepo = mock<WindowInfoRepo>()
-        whenever(mockRepo.currentWindowMetrics()).thenReturn(expected)
+        whenever(mockRepo.currentWindowMetrics).thenReturn(expected)
         val unitUnderTest = WindowInfoRepoJavaAdapter(mockRepo)
 
-        val acutal = unitUnderTest.currentWindowMetrics()
+        val acutal = unitUnderTest.currentWindowMetrics
 
         assertEquals(expected, acutal)
     }
@@ -54,10 +54,10 @@
     public fun testMaximumWindowMetrics_delegatesToRepo() {
         val expected = WindowMetrics(Rect(0, 1, 2, 3))
         val mockRepo = mock<WindowInfoRepo>()
-        whenever(mockRepo.maximumWindowMetrics()).thenReturn(expected)
+        whenever(mockRepo.maximumWindowMetrics).thenReturn(expected)
         val unitUnderTest = WindowInfoRepoJavaAdapter(mockRepo)
 
-        val acutal = unitUnderTest.maximumWindowMetrics()
+        val acutal = unitUnderTest.maximumWindowMetrics
 
         assertEquals(expected, acutal)
     }
@@ -71,7 +71,7 @@
         )
         val expected = WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()
         val mockRepo = mock<WindowInfoRepo>()
-        whenever(mockRepo.windowLayoutInfo()).thenReturn(flowOf(expected))
+        whenever(mockRepo.windowLayoutInfo).thenReturn(flowOf(expected))
         val unitUnderTest = WindowInfoRepoJavaAdapter(mockRepo)
         val testConsumer = TestConsumer<WindowLayoutInfo>()
 
@@ -89,7 +89,7 @@
         )
         val expected = WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()
         val mockRepo = mock<WindowInfoRepo>()
-        whenever(mockRepo.windowLayoutInfo()).thenReturn(flowOf(expected))
+        whenever(mockRepo.windowLayoutInfo).thenReturn(flowOf(expected))
         val unitUnderTest = WindowInfoRepoJavaAdapter(mockRepo)
         val testConsumer = TestConsumer<WindowLayoutInfo>()
 
@@ -110,7 +110,7 @@
         val info = WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()
         val mockRepo = mock<WindowInfoRepo>()
         val channel = Channel<WindowLayoutInfo>()
-        whenever(mockRepo.windowLayoutInfo()).thenReturn(channel.receiveAsFlow())
+        whenever(mockRepo.windowLayoutInfo).thenReturn(channel.receiveAsFlow())
         val unitUnderTest = WindowInfoRepoJavaAdapter(mockRepo)
         val testConsumer = TestConsumer<WindowLayoutInfo>()
 
diff --git a/window/window-java/src/main/java/androidx/window/java/WindowInfoRepoJavaAdapter.kt b/window/window-java/src/main/java/androidx/window/java/WindowInfoRepoJavaAdapter.kt
index 561d759..80b3c1d 100644
--- a/window/window-java/src/main/java/androidx/window/java/WindowInfoRepoJavaAdapter.kt
+++ b/window/window-java/src/main/java/androidx/window/java/WindowInfoRepoJavaAdapter.kt
@@ -42,7 +42,7 @@
     /**
      * Register a listener to consume [WindowLayoutInfo] values. If the same consumer is
      * registered twice then this method is a no-op.
-     * @see WindowInfoRepo.windowLayoutInfo
+     * @see WindowInfoRepo.getWindowLayoutInfo
      */
     public fun addWindowLayoutInfoListener(
         executor: Executor,
@@ -52,7 +52,7 @@
             if (consumerToJobMap[consumer] == null) {
                 val scope = CoroutineScope(executor.asCoroutineDispatcher())
                 consumerToJobMap[consumer] = scope.launch {
-                    repo.windowLayoutInfo().collect(consumer::accept)
+                    repo.windowLayoutInfo.collect(consumer::accept)
                 }
             }
         }
@@ -61,7 +61,7 @@
     /**
      * Remove a listener to stop consuming [WindowLayoutInfo] values. If the listener has already
      * been removed then this is a no-op.
-     * @see WindowInfoRepo.windowLayoutInfo
+     * @see WindowInfoRepo.getWindowLayoutInfo
      */
     public fun removeWindowLayoutInfoListener(consumer: Consumer<WindowLayoutInfo>) {
         lock.withLock {
diff --git a/window/window-rxjava2/src/androidTest/java/androidx/window/rxjava2/WindowInfoRepoRxTest.kt b/window/window-rxjava2/src/androidTest/java/androidx/window/rxjava2/WindowInfoRepoRxTest.kt
index 5ca2325..214ca23 100644
--- a/window/window-rxjava2/src/androidTest/java/androidx/window/rxjava2/WindowInfoRepoRxTest.kt
+++ b/window/window-rxjava2/src/androidTest/java/androidx/window/rxjava2/WindowInfoRepoRxTest.kt
@@ -41,7 +41,7 @@
         )
         val expected = WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()
         val mockRepo = mock<WindowInfoRepo>()
-        whenever(mockRepo.windowLayoutInfo()).thenReturn(flowOf(expected))
+        whenever(mockRepo.windowLayoutInfo).thenReturn(flowOf(expected))
 
         val testSubscriber = mockRepo.windowLayoutInfoObservable().test()
 
@@ -58,7 +58,7 @@
         )
         val expected = WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()
         val mockRepo = mock<WindowInfoRepo>()
-        whenever(mockRepo.windowLayoutInfo()).thenReturn(flowOf(expected))
+        whenever(mockRepo.windowLayoutInfo).thenReturn(flowOf(expected))
 
         val testSubscriber = mockRepo.windowLayoutInfoFlowable().test()
 
diff --git a/window/window-rxjava2/src/main/java/androidx/window/rxjava2/WindowInfoRepoRx.kt b/window/window-rxjava2/src/main/java/androidx/window/rxjava2/WindowInfoRepoRx.kt
index 5ee31b8..940cf75 100644
--- a/window/window-rxjava2/src/main/java/androidx/window/rxjava2/WindowInfoRepoRx.kt
+++ b/window/window-rxjava2/src/main/java/androidx/window/rxjava2/WindowInfoRepoRx.kt
@@ -27,18 +27,18 @@
 
 /**
  * Return an [Observable] stream of [WindowLayoutInfo].
- * @see WindowInfoRepo.windowLayoutInfo
+ * @see WindowInfoRepo.getWindowLayoutInfo
  */
 @ExperimentalCoroutinesApi
 public fun WindowInfoRepo.windowLayoutInfoObservable(): Observable<WindowLayoutInfo> {
-    return windowLayoutInfo().asObservable()
+    return windowLayoutInfo.asObservable()
 }
 
 /**
  * Return a [Flowable] stream of [WindowLayoutInfo].
- * @see WindowInfoRepo.windowLayoutInfo
+ * @see WindowInfoRepo.getWindowLayoutInfo
  */
 @ExperimentalCoroutinesApi
 public fun WindowInfoRepo.windowLayoutInfoFlowable(): Flowable<WindowLayoutInfo> {
-    return windowLayoutInfo().asFlowable()
+    return windowLayoutInfo.asFlowable()
 }
\ No newline at end of file
diff --git a/window/window-rxjava3/src/androidTest/java/androidx/window/rxjava3/WindowInfoRepoRxTest.kt b/window/window-rxjava3/src/androidTest/java/androidx/window/rxjava3/WindowInfoRepoRxTest.kt
index 8ca7e0b..b02f00d 100644
--- a/window/window-rxjava3/src/androidTest/java/androidx/window/rxjava3/WindowInfoRepoRxTest.kt
+++ b/window/window-rxjava3/src/androidTest/java/androidx/window/rxjava3/WindowInfoRepoRxTest.kt
@@ -43,7 +43,7 @@
         )
         val expected = WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()
         val mockRepo = mock<WindowInfoRepo>()
-        whenever(mockRepo.windowLayoutInfo()).thenReturn(flowOf(expected))
+        whenever(mockRepo.windowLayoutInfo).thenReturn(flowOf(expected))
 
         val testSubscriber = mockRepo.windowLayoutInfoObservable().test()
 
@@ -60,7 +60,7 @@
         )
         val expected = WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()
         val mockRepo = mock<WindowInfoRepo>()
-        whenever(mockRepo.windowLayoutInfo()).thenReturn(flowOf(expected))
+        whenever(mockRepo.windowLayoutInfo).thenReturn(flowOf(expected))
 
         val testSubscriber = mockRepo.windowLayoutInfoFlowable().test()
 
diff --git a/window/window-rxjava3/src/main/java/androidx/window/rxjava3/WindowInfoRepoRx.kt b/window/window-rxjava3/src/main/java/androidx/window/rxjava3/WindowInfoRepoRx.kt
index 26cacef..d9ef9e6 100644
--- a/window/window-rxjava3/src/main/java/androidx/window/rxjava3/WindowInfoRepoRx.kt
+++ b/window/window-rxjava3/src/main/java/androidx/window/rxjava3/WindowInfoRepoRx.kt
@@ -27,18 +27,18 @@
 
 /**
  * Return an [Observable] stream of [WindowLayoutInfo].
- * @see WindowInfoRepo.windowLayoutInfo
+ * @see WindowInfoRepo.getWindowLayoutInfo
  */
 @ExperimentalCoroutinesApi
 public fun WindowInfoRepo.windowLayoutInfoObservable(): Observable<WindowLayoutInfo> {
-    return windowLayoutInfo().asObservable()
+    return windowLayoutInfo.asObservable()
 }
 
 /**
  * Return a [Flowable] stream of [WindowLayoutInfo].
- * @see WindowInfoRepo.windowLayoutInfo
+ * @see WindowInfoRepo.getWindowLayoutInfo
  */
 @ExperimentalCoroutinesApi
 public fun WindowInfoRepo.windowLayoutInfoFlowable(): Flowable<WindowLayoutInfo> {
-    return windowLayoutInfo().asFlowable()
+    return windowLayoutInfo.asFlowable()
 }
diff --git a/window/window-testing/api/current.txt b/window/window-testing/api/current.txt
new file mode 100644
index 0000000..e0d15ce
--- /dev/null
+++ b/window/window-testing/api/current.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.window.test {
+
+  public final class WindowLayoutInfoPublisherRule implements org.junit.rules.TestRule {
+    ctor public WindowLayoutInfoPublisherRule();
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
+    method public void overrideWindowLayoutInfo(androidx.window.WindowLayoutInfo info);
+  }
+
+}
+
diff --git a/window/window-testing/api/public_plus_experimental_current.txt b/window/window-testing/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..f00e796
--- /dev/null
+++ b/window/window-testing/api/public_plus_experimental_current.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.window.test {
+
+  public final class WindowLayoutInfoPublisherRule implements org.junit.rules.TestRule {
+    ctor public WindowLayoutInfoPublisherRule();
+    method @kotlinx.coroutines.ExperimentalCoroutinesApi public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
+    method public void overrideWindowLayoutInfo(androidx.window.WindowLayoutInfo info);
+  }
+
+}
+
diff --git a/window/window-testing/api/res-current.txt b/window/window-testing/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/window/window-testing/api/res-current.txt
diff --git a/window/window-testing/api/restricted_current.txt b/window/window-testing/api/restricted_current.txt
new file mode 100644
index 0000000..e0d15ce
--- /dev/null
+++ b/window/window-testing/api/restricted_current.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.window.test {
+
+  public final class WindowLayoutInfoPublisherRule implements org.junit.rules.TestRule {
+    ctor public WindowLayoutInfoPublisherRule();
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
+    method public void overrideWindowLayoutInfo(androidx.window.WindowLayoutInfo info);
+  }
+
+}
+
diff --git a/window/window-testing/build.gradle b/window/window-testing/build.gradle
new file mode 100644
index 0000000..5a706af
--- /dev/null
+++ b/window/window-testing/build.gradle
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import androidx.build.LibraryVersions
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    defaultConfig {
+        multiDexEnabled = true
+    }
+}
+
+dependencies {
+    api(KOTLIN_STDLIB)
+    api(project(":window:window"))
+    api(JUNIT)
+    implementation("androidx.core:core:1.3.2")
+
+    androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    androidTestImplementation(ANDROIDX_TEST_RULES)
+    androidTestImplementation(KOTLIN_COROUTINES_TEST)
+    androidTestImplementation(MULTIDEX)
+    androidTestImplementation(TRUTH)
+}
+
+androidx {
+    name = "WindowManager Test Library"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenVersion = LibraryVersions.WINDOW
+    mavenGroup = LibraryGroups.WINDOW
+    inceptionYear = "2021"
+    description = "WindowManager Test Library"
+}
+
+// Allow usage of Kotlin's @OptIn.
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
+    }
+}
\ No newline at end of file
diff --git a/window/window-testing/src/androidTest/AndroidManifest.xml b/window/window-testing/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..7d587da
--- /dev/null
+++ b/window/window-testing/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.window.test">
+    <application>
+        <activity android:name="TestActivity"/>
+    </application>
+</manifest>
diff --git a/window/window/src/main/java/androidx/window/WindowServices.kt b/window/window-testing/src/androidTest/java/androidx/window/test/TestActivity.kt
similarity index 60%
rename from window/window/src/main/java/androidx/window/WindowServices.kt
rename to window/window-testing/src/androidTest/java/androidx/window/test/TestActivity.kt
index fd3881d..3d934fd 100644
--- a/window/window/src/main/java/androidx/window/WindowServices.kt
+++ b/window/window-testing/src/androidTest/java/androidx/window/test/TestActivity.kt
@@ -13,21 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-@file:JvmName("WindowServices")
 
-package androidx.window
+package androidx.window.test
 
 import android.app.Activity
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 /**
- * Provide an instance of [WindowInfoRepo] that is associated to the given [Activity]
+ * A test [Activity] for testing purposes.
  */
-@ExperimentalCoroutinesApi
-public fun Activity.windowInfoRepository(): WindowInfoRepo {
-    return WindowInfoRepoImp(
-        this,
-        WindowBoundsHelper.instance,
-        ExtensionWindowBackend.getInstance(this)
-    )
-}
+public class TestActivity : Activity()
\ No newline at end of file
diff --git a/window/window-testing/src/androidTest/java/androidx/window/test/WindowLayoutInfoPublisherRuleTest.kt b/window/window-testing/src/androidTest/java/androidx/window/test/WindowLayoutInfoPublisherRuleTest.kt
new file mode 100644
index 0000000..a93682b
--- /dev/null
+++ b/window/window-testing/src/androidTest/java/androidx/window/test/WindowLayoutInfoPublisherRuleTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.test
+
+import android.graphics.Rect
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.window.DisplayFeature
+import androidx.window.WindowLayoutInfo
+import androidx.window.windowInfoRepository
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toCollection
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+public class WindowLayoutInfoPublisherRuleTest {
+
+    private val activityRule = ActivityScenarioRule(TestActivity::class.java)
+    private val publisherRule = WindowLayoutInfoPublisherRule()
+
+    @get:Rule
+    public val testRule: TestRule
+
+    init {
+        testRule = RuleChain.outerRule(publisherRule).around(activityRule)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    public fun testWindowLayoutInfo_relayValue(): Unit = runBlockingTest {
+        val expected = WindowLayoutInfo.Builder().setDisplayFeatures(emptyList()).build()
+        activityRule.scenario.onActivity { activity ->
+            val value = async {
+                activity.windowInfoRepository().windowLayoutInfo.first()
+            }
+            publisherRule.overrideWindowLayoutInfo(expected)
+            runBlockingTest {
+                val actual = value.await()
+                assertEquals(expected, actual)
+            }
+        }
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    public fun testWindowLayoutInfo_multipleValues(): Unit = runBlockingTest {
+        val feature1 = object : DisplayFeature {
+            override val bounds: Rect
+                get() = Rect()
+        }
+        val feature2 = object : DisplayFeature {
+            override val bounds: Rect
+                get() = Rect()
+        }
+        val expected1 = WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature1)).build()
+        val expected2 = WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature2)).build()
+        activityRule.scenario.onActivity { activity ->
+            val values = mutableListOf<WindowLayoutInfo>()
+            val value = async {
+                activity.windowInfoRepository().windowLayoutInfo.take(4).toCollection(values)
+            }
+            publisherRule.overrideWindowLayoutInfo(expected1)
+            publisherRule.overrideWindowLayoutInfo(expected2)
+            publisherRule.overrideWindowLayoutInfo(expected1)
+            publisherRule.overrideWindowLayoutInfo(expected2)
+            runBlockingTest {
+                assertEquals(
+                    listOf(expected1, expected2, expected1, expected2),
+                    value.await().toList()
+                )
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/window/window-testing/src/main/AndroidManifest.xml b/window/window-testing/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4bd698f
--- /dev/null
+++ b/window/window-testing/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.window.test">
+</manifest>
\ No newline at end of file
diff --git a/window/window-testing/src/main/java/androidx/window/test/PublishLayoutInfoRepo.kt b/window/window-testing/src/main/java/androidx/window/test/PublishLayoutInfoRepo.kt
new file mode 100644
index 0000000..b3591df
--- /dev/null
+++ b/window/window-testing/src/main/java/androidx/window/test/PublishLayoutInfoRepo.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.test
+
+import androidx.window.WindowInfoRepo
+import androidx.window.WindowLayoutInfo
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+
+internal class PublishLayoutInfoRepo(
+    private val core: WindowInfoRepo,
+    private val flow: MutableSharedFlow<WindowLayoutInfo>
+) : WindowInfoRepo by core {
+    override val windowLayoutInfo: Flow<WindowLayoutInfo>
+        get() = flow
+}
\ No newline at end of file
diff --git a/window/window-testing/src/main/java/androidx/window/test/PublishWindowInfoRepoDecorator.kt b/window/window-testing/src/main/java/androidx/window/test/PublishWindowInfoRepoDecorator.kt
new file mode 100644
index 0000000..7cf34ba
--- /dev/null
+++ b/window/window-testing/src/main/java/androidx/window/test/PublishWindowInfoRepoDecorator.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.test
+
+import androidx.window.WindowInfoRepo
+import androidx.window.WindowInfoRepoDecorator
+import androidx.window.WindowLayoutInfo
+import kotlinx.coroutines.flow.MutableSharedFlow
+
+internal class PublishWindowInfoRepoDecorator(
+    private val flow: MutableSharedFlow<WindowLayoutInfo>
+) : WindowInfoRepoDecorator {
+    override fun decorate(repo: WindowInfoRepo): WindowInfoRepo {
+        return PublishLayoutInfoRepo(repo, flow)
+    }
+}
\ No newline at end of file
diff --git a/window/window-testing/src/main/java/androidx/window/test/WindowLayoutInfoPublisher.kt b/window/window-testing/src/main/java/androidx/window/test/WindowLayoutInfoPublisher.kt
new file mode 100644
index 0000000..2261e68
--- /dev/null
+++ b/window/window-testing/src/main/java/androidx/window/test/WindowLayoutInfoPublisher.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.test
+
+import androidx.window.WindowInfoRepo
+import androidx.window.WindowLayoutInfo
+import androidx.window.WindowMetrics
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+
+internal class WindowLayoutInfoPublisher(
+    private val core: WindowInfoRepo,
+    private val flow: StateFlow<WindowLayoutInfo>
+) : WindowInfoRepo {
+
+    override val currentWindowMetrics: WindowMetrics
+        get() {
+            return core.currentWindowMetrics
+        }
+
+    override val maximumWindowMetrics: WindowMetrics
+        get() {
+            return core.maximumWindowMetrics
+        }
+
+    override val windowLayoutInfo: Flow<WindowLayoutInfo>
+        get() {
+            return flow
+        }
+}
\ No newline at end of file
diff --git a/window/window-testing/src/main/java/androidx/window/test/WindowLayoutInfoPublisherRule.kt b/window/window-testing/src/main/java/androidx/window/test/WindowLayoutInfoPublisherRule.kt
new file mode 100644
index 0000000..f9e92fc
--- /dev/null
+++ b/window/window-testing/src/main/java/androidx/window/test/WindowLayoutInfoPublisherRule.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.test
+
+import androidx.window.WindowInfoRepo
+import androidx.window.WindowLayoutInfo
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
+import kotlinx.coroutines.flow.MutableSharedFlow
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * A [TestRule] to help test consuming a stream of [WindowLayoutInfo] values.
+ * [WindowLayoutInfoPublisherRule] allows you to push through different [WindowLayoutInfo] values
+ * on demand.
+ *
+ * Here are some recommended testing scenarios.
+ *
+ * To test the scenario where no [WindowLayoutInfo] is ever emitted.  Just set the rule as a
+ * standard rule.
+ *
+ * To test sending a generic feature build your own [WindowLayoutInfo] and publish it through the
+ * method [WindowLayoutInfoPublisherRule.overrideWindowLayoutInfo].
+ *
+ * Some helper methods are provided to test the following scenarios.
+ * <ul>
+ *     <li>A fold in the middle and the dimension matches the shortest window dimension.</li>
+ *     <li>A fold in the middle and the dimension matches the longest window dimension.</li>
+ *     <li>A fold in the middle and has vertical orientation.</li>
+ *     <li>A fold in the middle and has horizontal orientation.</li>
+ * </ul>
+ */
+public class WindowLayoutInfoPublisherRule() : TestRule {
+
+    private val flow = MutableSharedFlow<WindowLayoutInfo>(
+        extraBufferCapacity = 1,
+        onBufferOverflow = DROP_OLDEST
+    )
+    private val overrideServices = PublishWindowInfoRepoDecorator(flow)
+
+    @ExperimentalCoroutinesApi
+    override fun apply(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                WindowInfoRepo.overrideDecorator(overrideServices)
+                base.evaluate()
+                WindowInfoRepo.reset()
+            }
+        }
+    }
+
+    /**
+     * Send an arbitrary [WindowLayoutInfo] through
+     * [androidx.window.WindowInfoRepo.windowLayoutInfo]. Each event is sent only once.
+     */
+    public fun overrideWindowLayoutInfo(info: WindowLayoutInfo) {
+        flow.tryEmit(info)
+    }
+}
\ No newline at end of file
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index c82eaa4..d5386e1 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -41,9 +41,16 @@
   }
 
   public interface WindowInfoRepo {
-    method public androidx.window.WindowMetrics currentWindowMetrics();
-    method public androidx.window.WindowMetrics maximumWindowMetrics();
-    method public kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> windowLayoutInfo();
+    method public androidx.window.WindowMetrics getCurrentWindowMetrics();
+    method public androidx.window.WindowMetrics getMaximumWindowMetrics();
+    method public kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> getWindowLayoutInfo();
+    property public abstract androidx.window.WindowMetrics currentWindowMetrics;
+    property public abstract androidx.window.WindowMetrics maximumWindowMetrics;
+    property public abstract kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> windowLayoutInfo;
+    field public static final androidx.window.WindowInfoRepo.Companion Companion;
+  }
+
+  public static final class WindowInfoRepo.Companion {
   }
 
   public final class WindowLayoutInfo {
@@ -72,8 +79,5 @@
     property public final android.graphics.Rect bounds;
   }
 
-  public final class WindowServices {
-  }
-
 }
 
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index c6788d2..5ef6e22 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -2,6 +2,7 @@
 package androidx.window {
 
   public final class ActivityExtKt {
+    method @kotlinx.coroutines.ExperimentalCoroutinesApi public static androidx.window.WindowInfoRepo windowInfoRepository(android.app.Activity);
   }
 
   public interface DisplayFeature {
@@ -41,9 +42,26 @@
   }
 
   public interface WindowInfoRepo {
-    method public androidx.window.WindowMetrics currentWindowMetrics();
-    method public androidx.window.WindowMetrics maximumWindowMetrics();
-    method public kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> windowLayoutInfo();
+    method @kotlinx.coroutines.ExperimentalCoroutinesApi public default static androidx.window.WindowInfoRepo create(android.app.Activity activity);
+    method public androidx.window.WindowMetrics getCurrentWindowMetrics();
+    method public androidx.window.WindowMetrics getMaximumWindowMetrics();
+    method public kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> getWindowLayoutInfo();
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public default static void overrideDecorator(androidx.window.WindowInfoRepoDecorator overridingDecorator);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public default static void reset();
+    property public abstract androidx.window.WindowMetrics currentWindowMetrics;
+    property public abstract androidx.window.WindowMetrics maximumWindowMetrics;
+    property public abstract kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> windowLayoutInfo;
+    field public static final androidx.window.WindowInfoRepo.Companion Companion;
+  }
+
+  public static final class WindowInfoRepo.Companion {
+    method @kotlinx.coroutines.ExperimentalCoroutinesApi public androidx.window.WindowInfoRepo create(android.app.Activity activity);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void overrideDecorator(androidx.window.WindowInfoRepoDecorator overridingDecorator);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void reset();
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public interface WindowInfoRepoDecorator {
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public androidx.window.WindowInfoRepo decorate(androidx.window.WindowInfoRepo repo);
   }
 
   public final class WindowLayoutInfo {
@@ -72,9 +90,5 @@
     property public final android.graphics.Rect bounds;
   }
 
-  public final class WindowServices {
-    method @kotlinx.coroutines.ExperimentalCoroutinesApi public static androidx.window.WindowInfoRepo windowInfoRepository(android.app.Activity);
-  }
-
 }
 
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index c82eaa4..0f592ee 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -41,9 +41,24 @@
   }
 
   public interface WindowInfoRepo {
-    method public androidx.window.WindowMetrics currentWindowMetrics();
-    method public androidx.window.WindowMetrics maximumWindowMetrics();
-    method public kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> windowLayoutInfo();
+    method public androidx.window.WindowMetrics getCurrentWindowMetrics();
+    method public androidx.window.WindowMetrics getMaximumWindowMetrics();
+    method public kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> getWindowLayoutInfo();
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public default static void overrideDecorator(androidx.window.WindowInfoRepoDecorator overridingDecorator);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public default static void reset();
+    property public abstract androidx.window.WindowMetrics currentWindowMetrics;
+    property public abstract androidx.window.WindowMetrics maximumWindowMetrics;
+    property public abstract kotlinx.coroutines.flow.Flow<androidx.window.WindowLayoutInfo> windowLayoutInfo;
+    field public static final androidx.window.WindowInfoRepo.Companion Companion;
+  }
+
+  public static final class WindowInfoRepo.Companion {
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void overrideDecorator(androidx.window.WindowInfoRepoDecorator overridingDecorator);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void reset();
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public interface WindowInfoRepoDecorator {
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public androidx.window.WindowInfoRepo decorate(androidx.window.WindowInfoRepo repo);
   }
 
   public final class WindowLayoutInfo {
@@ -72,8 +87,5 @@
     property public final android.graphics.Rect bounds;
   }
 
-  public final class WindowServices {
-  }
-
 }
 
diff --git a/window/window/src/androidTest/java/androidx/window/WindowInfoRepoImpTest.kt b/window/window/src/androidTest/java/androidx/window/WindowInfoRepoImpTest.kt
index 724e6e3..6310418 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowInfoRepoImpTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/WindowInfoRepoImpTest.kt
@@ -49,7 +49,7 @@
             )
             val expectedBounds = windowBoundsHelper.computeCurrentWindowBounds(testActivity)
             val expected = WindowMetrics(expectedBounds)
-            val actual = repo.currentWindowMetrics()
+            val actual = repo.currentWindowMetrics
             assertEquals(expected, actual)
         }
     }
@@ -65,7 +65,7 @@
             )
             val expectedBounds = windowBoundsHelper.computeMaximumWindowBounds(testActivity)
             val expected = WindowMetrics(expectedBounds)
-            val actual = repo.maximumWindowMetrics()
+            val actual = repo.maximumWindowMetrics
             assertEquals(expected, actual)
         }
     }
@@ -82,7 +82,7 @@
             )
             val collector = TestConsumer<WindowLayoutInfo>()
             testScope.launch {
-                repo.windowLayoutInfo().collect(collector::accept)
+                repo.windowLayoutInfo.collect(collector::accept)
             }
             fakeBackend.triggerSignal(WindowLayoutInfo(emptyList()))
             collector.assertValue(WindowLayoutInfo(emptyList()))
diff --git a/window/window/src/main/java/androidx/window/ActivityExt.kt b/window/window/src/main/java/androidx/window/ActivityExt.kt
index a5149ca..ab6fcf9 100644
--- a/window/window/src/main/java/androidx/window/ActivityExt.kt
+++ b/window/window/src/main/java/androidx/window/ActivityExt.kt
@@ -17,6 +17,8 @@
 
 import android.app.Activity
 import android.os.IBinder
+import android.os.Looper
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 /**
  * A utility method [Activity] to return an optional [IBinder] window token from an [Activity].
@@ -24,3 +26,29 @@
 internal fun getActivityWindowToken(activity: Activity?): IBinder? {
     return activity?.window?.attributes?.token
 }
+
+internal inline fun <reified T> Activity.getTag(id: Int): T? {
+    return window.decorView.getTag(id) as? T
+}
+
+/**
+ * Checks to see if an object of type [T] is associated with the tag [id]. If it is available
+ * then it is returned. Otherwise set an object crated using the [producer] and return that value.
+ * @return object associated with the tag.
+ */
+internal inline fun <reified T> Activity.getOrCreateTag(id: Int, producer: () -> T): T {
+    return (window.decorView.getTag(id) as? T) ?: run {
+        assert(Looper.getMainLooper() == Looper.myLooper())
+        val value = producer()
+        window.decorView.setTag(id, value)
+        value
+    }
+}
+
+/**
+ * Provide an instance of [WindowInfoRepo] that is associated to the given [Activity]
+ */
+@ExperimentalCoroutinesApi
+public fun Activity.windowInfoRepository(): WindowInfoRepo {
+    return WindowInfoRepo.create(this)
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/WindowBoundsHelper.kt b/window/window/src/main/java/androidx/window/WindowBoundsHelper.kt
index 6434d39..4a53719 100644
--- a/window/window/src/main/java/androidx/window/WindowBoundsHelper.kt
+++ b/window/window/src/main/java/androidx/window/WindowBoundsHelper.kt
@@ -27,6 +27,9 @@
 import android.view.Display
 import android.view.DisplayCutout
 import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
 import androidx.annotation.VisibleForTesting
 import androidx.window.ActivityCompatHelperApi24.isInMultiWindowMode
 import androidx.window.ActivityCompatHelperApi30.currentWindowBounds
@@ -83,7 +86,8 @@
      * @see android.view.WindowManager.getCurrentWindowMetrics
      * @see android.view.WindowMetrics.getBounds
      */
-    open fun computeCurrentWindowBounds(activity: Activity): Rect {
+    @RestrictTo(LIBRARY)
+    public open fun computeCurrentWindowBounds(activity: Activity): Rect {
         return if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
             currentWindowBounds(activity)
         } else if (Build.VERSION.SDK_INT >= VERSION_CODES.Q) {
@@ -108,7 +112,8 @@
      *
      * @see android.view.WindowManager.getMaximumWindowMetrics
      */
-    open fun computeMaximumWindowBounds(activity: Activity): Rect {
+    @RestrictTo(LIBRARY_GROUP)
+    public open fun computeMaximumWindowBounds(activity: Activity): Rect {
         return if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
             maximumWindowBounds(activity)
         } else {
@@ -121,7 +126,7 @@
         }
     }
 
-    companion object {
+    internal companion object {
         private const val TAG = "WindowBoundsHelper"
         private val globalInstance = WindowBoundsHelper()
         private var testInstance: WindowBoundsHelper? = null
diff --git a/window/window/src/main/java/androidx/window/WindowInfoRepo.kt b/window/window/src/main/java/androidx/window/WindowInfoRepo.kt
index 053bdb3..7b3b623 100644
--- a/window/window/src/main/java/androidx/window/WindowInfoRepo.kt
+++ b/window/window/src/main/java/androidx/window/WindowInfoRepo.kt
@@ -16,7 +16,11 @@
 
 package androidx.window
 
+import android.app.Activity
 import android.content.Context
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 
 /**
@@ -41,7 +45,7 @@
      * @see maximumWindowMetrics
      * @see android.view.WindowManager.getCurrentWindowMetrics
      */
-    public fun currentWindowMetrics(): WindowMetrics
+    public val currentWindowMetrics: WindowMetrics
 
     /**
      * Returns the largest [WindowMetrics] an app may expect in the current system state.
@@ -61,17 +65,63 @@
      * areas of the display are not available to windows created for the associated [Context].
      * For example, devices with foldable displays that wrap around the enclosure may split the
      * physical display into different regions, one for the front and one for the back, each acting
-     * as different logical displays. In this case [getMaximumWindowMetrics] would return
+     * as different logical displays. In this case [maximumWindowMetrics] would return
      * the region describing the side of the device the associated [context's][Context]
      * window is placed.
      *
      * @see currentWindowMetrics
      * @see android.view.WindowManager.getMaximumWindowMetrics
      */
-    public fun maximumWindowMetrics(): WindowMetrics
+    public val maximumWindowMetrics: WindowMetrics
 
     /**
      * A [Flow] of [WindowLayoutInfo] that contains all the available features.
      */
-    public fun windowLayoutInfo(): Flow<WindowLayoutInfo>
+    public val windowLayoutInfo: Flow<WindowLayoutInfo>
+
+    public companion object {
+
+        private var decorator: WindowInfoRepoDecorator = EmptyDecorator
+
+        @JvmStatic
+        @ExperimentalCoroutinesApi
+        public fun create(activity: Activity): WindowInfoRepo {
+            val taggedRepo = activity.getTag(R.id.androidx_window_activity_scope) as? WindowInfoRepo
+            val repo = taggedRepo ?: activity.getOrCreateTag(R.id.androidx_window_activity_scope) {
+                WindowInfoRepoImp(
+                    activity,
+                    WindowBoundsHelper(),
+                    ExtensionWindowBackend.getInstance(activity)
+                )
+            }
+            return decorator.decorate(repo)
+        }
+
+        @JvmStatic
+        @RestrictTo(LIBRARY_GROUP)
+        public fun overrideDecorator(overridingDecorator: WindowInfoRepoDecorator) {
+            decorator = overridingDecorator
+        }
+
+        @JvmStatic
+        @RestrictTo(LIBRARY_GROUP)
+        public fun reset() {
+            decorator = EmptyDecorator
+        }
+    }
 }
+
+@RestrictTo(LIBRARY_GROUP)
+public interface WindowInfoRepoDecorator {
+    /**
+     * Returns an instance of [WindowInfoRepo] associated to the [Activity]
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public fun decorate(repo: WindowInfoRepo): WindowInfoRepo
+}
+
+private object EmptyDecorator : WindowInfoRepoDecorator {
+    override fun decorate(repo: WindowInfoRepo): WindowInfoRepo {
+        return repo
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/WindowInfoRepoImp.kt b/window/window/src/main/java/androidx/window/WindowInfoRepoImp.kt
index 817459f..f3f0105 100644
--- a/window/window/src/main/java/androidx/window/WindowInfoRepoImp.kt
+++ b/window/window/src/main/java/androidx/window/WindowInfoRepoImp.kt
@@ -58,9 +58,10 @@
      * @see maximumWindowMetrics
      * @see android.view.WindowManager.getCurrentWindowMetrics
      */
-    override fun currentWindowMetrics(): WindowMetrics {
-        return WindowMetrics(windowBoundsHelper.computeCurrentWindowBounds(activity))
-    }
+    override val currentWindowMetrics: WindowMetrics
+        get() {
+            return WindowMetrics(windowBoundsHelper.computeCurrentWindowBounds(activity))
+        }
 
     /**
      * Returns the largest [WindowMetrics] an app may expect in the current system state.
@@ -87,18 +88,20 @@
      * @see currentWindowMetrics
      * @see android.view.WindowManager.getMaximumWindowMetrics
      */
-    override fun maximumWindowMetrics(): WindowMetrics {
-        return WindowMetrics(windowBoundsHelper.computeMaximumWindowBounds(activity))
-    }
+    override val maximumWindowMetrics: WindowMetrics
+        get() {
+            return WindowMetrics(windowBoundsHelper.computeMaximumWindowBounds(activity))
+        }
 
     /**
      * A [Flow] of window layout changes in the current visual [Context].
      *
      * @see Activity.onAttachedToWindow
      */
-    override fun windowLayoutInfo(): Flow<WindowLayoutInfo> = callbackFlow {
-        val callback = Consumer<WindowLayoutInfo> { info -> offer(info) }
-        windowBackend.registerLayoutChangeCallback(activity, Runnable::run, callback)
-        awaitClose { windowBackend.unregisterLayoutChangeCallback(callback) }
-    }.buffer(capacity = UNLIMITED)
+    override val windowLayoutInfo: Flow<WindowLayoutInfo>
+        get() = callbackFlow {
+            val callback = Consumer<WindowLayoutInfo> { info -> offer(info) }
+            windowBackend.registerLayoutChangeCallback(activity, Runnable::run, callback)
+            awaitClose { windowBackend.unregisterLayoutChangeCallback(callback) }
+        }.buffer(capacity = UNLIMITED)
 }
diff --git a/window/window/src/main/res/values/ids.xml b/window/window/src/main/res/values/ids.xml
new file mode 100644
index 0000000..c49bffa
--- /dev/null
+++ b/window/window/src/main/res/values/ids.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <item name="androidx_window_activity_scope" type="id"/>
+</resources>
\ No newline at end of file