Merge "Make sure there is only 1 instance of each type element" into androidx-main
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index baf38e0..d5cae8d7 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -9,6 +9,9 @@
   - name: Fragment
     url: https://issuetracker.google.com/issues/new?component=460964&template=1182267
     about: File a bug or feature request for Fragment
+  - name: Lifecycle
+    url: https://issuetracker.google.com/issues/new?component=413132&template=1096619
+    about: File a bug or feature request for Lifecycle
   - name: Navigation
     url: https://issuetracker.google.com/issues/new?component=409828&template=1093757
     about: File a bug or feature request for Navigation
diff --git a/.github/workflows/presubmit.yml b/.github/workflows/presubmit.yml
index c1dee53d..891ae8d 100644
--- a/.github/workflows/presubmit.yml
+++ b/.github/workflows/presubmit.yml
@@ -191,6 +191,59 @@
         if: always()
         run: echo ::set-output name=status::${{ job.status }}
 
+  build-lifecycle:
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest]
+    runs-on: ${{ matrix.os }}
+    needs: [setup, lint]
+    outputs:
+      status: ${{ steps.output-status.outputs.status }}
+    env:
+      group-id: "lifecycle"
+    steps:
+      - name: "Checkout androidx repo"
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 1
+
+      - name: "Setup JDK 11"
+        id: setup-java
+        uses: actions/setup-java@v1
+        with:
+          java-version: "11"
+
+      - name: "Set environment variables"
+        shell: bash
+        run: |
+          set -x
+          echo "ANDROID_SDK_ROOT=$HOME/Library/Android/sdk" >> $GITHUB_ENV
+          echo "DIST_DIR=$HOME/dist" >> $GITHUB_ENV
+
+      - name: "./gradlew buildOnServer"
+        uses: eskatos/gradle-command-action@v1
+        env:
+          JAVA_HOME: ${{ steps.setup-java.outputs.path }}
+        with:
+          arguments: buildOnServer
+          build-root-directory: ${{ env.group-id }}
+          gradle-executable: ${{ env.group-id }}/gradlew
+          wrapper-directory: ${{ env.group-id }}/gradle/wrapper
+
+      - name: "Upload build artifacts"
+        continue-on-error: true
+        if: always()
+        uses: actions/upload-artifact@v2
+        with:
+          name: artifacts_${{ env.group-id }}
+          path: ~/dist
+
+      - name: "Report job status"
+        id: output-status
+        if: always()
+        run: echo ::set-output name=status::${{ job.status }}
+
   build-navigation:
     strategy:
       fail-fast: false
@@ -411,6 +464,7 @@
       lint,
       build-activity,
       build-fragment,
+      build-lifecycle,
       build-navigation,
       build-paging,
       build-room,
@@ -425,6 +479,7 @@
           if [ "${{ needs.lint.outputs.status }}" == "success" ]            && \
             [ "${{ needs.build-activity.outputs.status }}" == "success" ]   && \
             [ "${{ needs.build-fragment.outputs.status }}" == "success" ]   && \
+            [ "${{ needs.build-lifecycle.outputs.status }}" == "success" ]  && \
             [ "${{ needs.build-navigation.outputs.status }}" == "success" ] && \
             [ "${{ needs.build-paging.outputs.status }}" == "success" ]     && \
             [ "${{ needs.build-room.outputs.status }}" == "success" ]       && \
diff --git a/.github/workflows/prune_artifacts.yml b/.github/workflows/prune_artifacts.yml
new file mode 100644
index 0000000..6d4171e
--- /dev/null
+++ b/.github/workflows/prune_artifacts.yml
@@ -0,0 +1,14 @@
+name: "Prune old artifacts"
+on:
+  schedule:
+    - cron: "0 * * * *"
+
+jobs:
+  delete-artifacts:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: kolpav/purge-artifacts-action@v1
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          expire-in: 7days
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4fc8df1..433e83b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,6 +12,7 @@
   - [Activity](https://developer.android.com/guide/components/activities/intro-activities)
   - [Biometric](https://developer.android.com/training/sign-in/biometric-auth)
   - [Fragment](https://developer.android.com/guide/components/fragments)
+  - [Lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle)
   - [Navigation](https://developer.android.com/guide/navigation)
   - [Paging](https://developer.android.com/topic/libraries/architecture/paging)
   - [Room](https://developer.android.com/topic/libraries/architecture/room)
@@ -48,6 +49,7 @@
   -- activity
   -- biometric
   -- fragment
+  -- lifecycle
   -- navigation
   -- paging
   -- room
diff --git a/README.md b/README.md
index c9828cb9..dcc6d1a 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@
 * [Activity](activity)
 * [Biometric](biometric)
 * [Fragment](fragment)
+* [Lifecycle](lifecycle)
 * [Navigation](navigation)
 * [Paging](paging)
 * [Room](room)
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
index db721f3..d743f82 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
@@ -37,7 +37,7 @@
 import java.util.Set;
 
 public class SetSchemaRequestTest {
-
+// @exportToFramework:startStrip()
     @AppSearchDocument
     static class Card {
         @AppSearchDocument.Uri
@@ -82,6 +82,7 @@
                 (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
         String mString;
     }
+// @exportToFramework:endStrip()
 
     private static Collection<String> getSchemaTypesFromSetSchemaRequest(SetSchemaRequest request) {
         HashSet<String> schemaTypes = new HashSet<>();
@@ -132,6 +133,7 @@
         assertThat(request.getSchemasNotVisibleToSystemUi()).containsExactly("Schema");
     }
 
+// @exportToFramework:startStrip()
     @Test
     public void testDataClassVisibilityForSystemUi_visible() throws Exception {
         // By default, the schema is visible.
@@ -154,6 +156,7 @@
                         Card.class, false).build();
         assertThat(request.getSchemasNotVisibleToSystemUi()).containsExactly("Card");
     }
+// @exportToFramework:endStrip()
 
     @Test
     public void testSchemaTypeVisibilityForPackage_visible() {
@@ -231,6 +234,7 @@
         assertThat(request.getSchemasVisibleToPackages()).isEmpty();
     }
 
+// @exportToFramework:startStrip()
     @Test
     public void testDataClassVisibilityForPackage_visible() throws Exception {
         // By default, the schema is not visible.
@@ -329,4 +333,5 @@
         assertThat(getSchemaTypesFromSetSchemaRequest(request)).containsExactly("Queen",
                 "King");
     }
+// @exportToFramework:endStrip()
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
index 44d780c..3b4bdb5 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
@@ -45,6 +45,8 @@
 import androidx.appsearch.localstorage.LocalStorage;
 import androidx.test.core.app.ApplicationProvider;
 
+import com.google.common.util.concurrent.MoreExecutors;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -107,12 +109,15 @@
         mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(emailSchema).build()).get();
     }
 
+// @exportToFramework:startStrip()
     @Test
     public void testSetSchema_dataClass() throws Exception {
         mDb1.setSchema(
                 new SetSchemaRequest.Builder().addDataClass(EmailDataClass.class).build()).get();
     }
+// @exportToFramework:endStrip()
 
+// @exportToFramework:startStrip()
     @Test
     public void testGetSchema() throws Exception {
         AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1")
@@ -158,6 +163,7 @@
         assertThat(actual1).isEqualTo(request1.getSchemas());
         assertThat(actual2).isEqualTo(request2.getSchemas());
     }
+// @exportToFramework:endStrip()
 
     @Test
     public void testPutDocuments() throws Exception {
@@ -179,6 +185,7 @@
         assertThat(result.getFailures()).isEmpty();
     }
 
+// @exportToFramework:startStrip()
     @Test
     public void testPutDocuments_dataClass() throws Exception {
         // Schema registration
@@ -196,6 +203,7 @@
         assertThat(result.getSuccesses()).containsExactly("uri1", null);
         assertThat(result.getFailures()).isEmpty();
     }
+// @exportToFramework:endStrip()
 
     @Test
     public void testUpdateSchema() throws Exception {
@@ -437,6 +445,7 @@
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
     }
 
+// @exportToFramework:startStrip()
     @Test
     public void testGetDocuments_dataClass() throws Exception {
         // Schema registration
@@ -459,6 +468,7 @@
         assertThat(inEmail.subject).isEqualTo(outEmail.subject);
         assertThat(inEmail.body).isEqualTo(outEmail.body);
     }
+// @exportToFramework:endStrip()
 
     @Test
     public void testQuery() throws Exception {
@@ -1285,4 +1295,72 @@
         assertThat(getResult.getFailures().get("uri1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
     }
+
+    @Test
+    public void testCloseAndReopen() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build());
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putDocuments(
+                new PutDocumentsRequest.Builder().addGenericDocument(inEmail).build()));
+
+        // close and re-open the appSearchSession
+        mDb1.close();
+        Context context = ApplicationProvider.getApplicationContext();
+        mDb1 = LocalStorage.createSearchSession(
+                new LocalStorage.SearchContext.Builder(context)
+                        .setDatabaseName(DB_NAME_1).build()).get();
+
+        // Query for the document
+        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail);
+    }
+
+    @Test
+    public void testCallAfterClose() throws Exception {
+
+        // Create a same-thread database by inject an executor which could help us maintain the
+        // execution order of those async tasks.
+        Context context = ApplicationProvider.getApplicationContext();
+        AppSearchSession sameThreadDb = LocalStorage.createSearchSession(
+                new LocalStorage.SearchContext.Builder(context)
+                        .setDatabaseName("sameThreadDb").build(),
+                MoreExecutors.newDirectExecutorService()).get();
+
+        try {
+            // Schema registration -- just mutate something
+            sameThreadDb.setSchema(
+                    new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
+
+            // Close the database. No further call will be allowed.
+            sameThreadDb.close();
+
+            // Try to query the closed database
+            // We are using the same-thread db here to make sure it has been closed.
+            IllegalStateException e = assertThrows(IllegalStateException.class, () ->
+                    sameThreadDb.query("query", new SearchSpec.Builder()
+                            .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                            .build()));
+            assertThat(e).hasMessageThat().contains("AppSearchSession has already been closed");
+        } finally {
+            // To clean the data that has been added in the test, need to re-open the session and
+            // set an empty schema.
+            AppSearchSession reopen = LocalStorage.createSearchSession(
+                    new LocalStorage.SearchContext.Builder(context)
+                            .setDatabaseName("sameThreadDb").build(),
+                    MoreExecutors.newDirectExecutorService()).get();
+            reopen.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        }
+    }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index a2c188c..0f34d6a 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -19,6 +19,7 @@
 import android.annotation.SuppressLint;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -210,4 +211,13 @@
     @NonNull
     ListenableFuture<Void> removeByQuery(
             @NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+
+    /**
+     * Closes the SearchSessionImpl to persists all update/delete requests to the disk.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    // TODO(b/175637134) when unhide it, extends Closeable and remove this method.
+    void close();
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index 28357af..049fc1f 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -450,6 +450,7 @@
         }
     }
 
+// @exportToFramework:startStrip()
     /**
      * Converts this GenericDocument into an instance of the provided data class.
      *
@@ -471,6 +472,7 @@
         DataClassFactory<T> factory = registry.getOrCreateFactory(dataClass);
         return factory.fromGenericDocument(this);
     }
+// @exportToFramework:endStrip()
 
     @Override
     public boolean equals(@Nullable Object other) {
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
index 7adee3c..1a481f1 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
@@ -69,6 +69,7 @@
             return this;
         }
 
+// @exportToFramework:startStrip()
         /**
          * Adds one or more documents to the request.
          *
@@ -113,6 +114,7 @@
             DataClassFactory<T> factory = registry.getOrCreateFactory(dataClass);
             return factory.toGenericDocument(dataClass);
         }
+// @exportToFramework:endStrip()
 
         /** Builds a new {@link PutDocumentsRequest}. */
         @NonNull
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
index f3c6a54..1cf99e8 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -299,6 +299,7 @@
             return this;
         }
 
+// @exportToFramework:startStrip()
         /**
          * Adds the Schema type of given data classes to the Schema type filter of
          * {@link SearchSpec} Entry. Only search for documents that have the specified schema types.
@@ -323,7 +324,9 @@
             addSchemaType(schemaTypes);
             return this;
         }
+// @exportToFramework:endStrip()
 
+// @exportToFramework:startStrip()
         /**
          * Adds the Schema type of given data classes to the Schema type filter of
          * {@link SearchSpec} Entry. Only search for documents that have the specified schema types.
@@ -340,6 +343,7 @@
             Preconditions.checkNotNull(dataClasses);
             return addSchemaByDataClass(Arrays.asList(dataClasses));
         }
+// @exportToFramework:endStrip()
 
         /**
          * Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
index 9f65a80..07df364 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -139,6 +139,7 @@
             return this;
         }
 
+// @exportToFramework:startStrip()
         /**
          * Adds one or more types to the schema.
          *
@@ -181,6 +182,7 @@
             }
             return addSchema(schemas);
         }
+// @exportToFramework:endStrip()
 
         /**
          * Sets visibility on system UI surfaces for the given {@code schemaType}.
@@ -243,6 +245,7 @@
             return this;
         }
 
+// @exportToFramework:startStrip()
         /**
          * Sets visibility on system UI surfaces for the given {@code dataClass}.
          *
@@ -287,6 +290,7 @@
             return setSchemaTypeVisibilityForPackage(factory.getSchemaType(), visible,
                     packageIdentifier);
         }
+// @exportToFramework:endStrip()
 
         /**
          * Configures the {@link SetSchemaRequest} to delete any existing documents that don't
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index ab2e767..69bdb81 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -58,11 +58,12 @@
                 print('Prune: remove "%s"' % abs_path)
                 os.remove(abs_path)
 
-    def _TransformAndCopyFile(self, source_path, dest_path, transform_func=None):
+    def _TransformAndCopyFile(
+            self, source_path, dest_path, transform_func=None, ignore_skips=False):
         with open(source_path, 'r') as fh:
             contents = fh.read()
 
-        if '@exportToFramework:skipFile()' in contents:
+        if not ignore_skips and '@exportToFramework:skipFile()' in contents:
             print('Skipping: "%s" -> "%s"' % (source_path, dest_path), file=sys.stderr)
             return
 
@@ -79,6 +80,12 @@
         subprocess.check_call(google_java_format_cmd, cwd=self._framework_appsearch_root)
 
     def _TransformCommonCode(self, contents):
+        # Apply strips
+        contents = re.sub(
+                r'\/\/ @exportToFramework:startStrip\(\).*?\/\/ @exportToFramework:endStrip\(\)',
+                '',
+                contents,
+                flags=re.DOTALL)
         return (contents
             .replace('androidx.appsearch.app', 'android.app.appsearch')
             .replace(
@@ -91,6 +98,7 @@
             .replace(
                     'androidx.annotation.VisibleForTesting',
                     'com.android.internal.annotations.VisibleForTesting')
+            .replace('androidx.annotation.', 'android.annotation.')
             .replace('androidx.collection.ArrayMap', 'android.util.ArrayMap')
             .replace('androidx.collection.ArraySet', 'android.util.ArraySet')
             .replace(
@@ -100,19 +108,48 @@
                     'androidx.core.util.Preconditions',
                     'com.android.internal.util.Preconditions')
             .replace('import androidx.annotation.RestrictTo;', '')
+            .replace('@RestrictTo(RestrictTo.Scope.LIBRARY)', '')
             .replace('@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)', '')
             .replace('ObjectsCompat.', 'Objects.')
-            .replace('androidx.', 'android.')
+            .replace('// @exportToFramework:skipFile()', '')
         )
 
     def _TransformTestCode(self, contents):
         contents = (contents
-            .replace('org.junit.Assert.assertThrows',
-                    'org.testng.Assert.expectThrows')
+            .replace('org.junit.Assert.assertThrows', 'org.testng.Assert.expectThrows')
             .replace('assertThrows(', 'expectThrows(')
+            .replace('androidx.appsearch.app.util.', 'com.android.server.appsearch.testing.')
+            .replace(
+                    'package androidx.appsearch.app.util;',
+                    'package com.android.server.appsearch.testing;')
+            .replace('AppSearchSession;', 'AppSearchSessionShim;')
+            .replace('AppSearchSession ', 'AppSearchSessionShim ')
+            .replace('GlobalSearchSession;', 'GlobalSearchSessionShim;')
+            .replace('GlobalSearchSession ', 'GlobalSearchSessionShim ')
+            .replace('SearchResults;', 'SearchResultsShim;')
+            .replace('SearchResults ', 'SearchResultsShim ')
+            .replace(
+                    'LocalStorage.createSearchSession(',
+                    'AppSearchSessionShimImpl.createSearchSession(')
+            .replace(
+                    'LocalStorage.createGlobalSearchSession(',
+                    'GlobalSearchSessionShimImpl.createGlobalSearchSession(')
+            .replace(
+                    'new LocalStorage.SearchContext.Builder(context)',
+                    'new LocalStorage.SearchContext.Builder()')
+            .replace(
+                    'new LocalStorage.GlobalSearchContext.Builder(context).build()',
+                    '')
+            .replace('LocalStorage.', 'AppSearchManager.')
         )
         return self._TransformCommonCode(contents)
 
+    def _TransformCtsTestCode(self, contents):
+        contents = self._TransformTestCode(contents)
+        return (contents
+                .replace('android.app.appsearch.test', 'com.android.cts.appsearch')
+        )
+
     def _TransformAndCopyFolder(self, source_dir, dest_dir, transform_func=None):
         for currentpath, folders, files in os.walk(source_dir):
             dir_rel_to_root = os.path.relpath(currentpath, source_dir)
@@ -180,12 +217,20 @@
         # Copy CTS tests
         print('~~~ Copying CTS tests ~~~')
         self._TransformAndCopyFolder(
-                cts_test_source_dir, cts_test_dest_dir, transform_func=self._TransformTestCode)
+                cts_test_source_dir, cts_test_dest_dir, transform_func=self._TransformCtsTestCode)
 
         # Copy test utils
         print('~~~ Copying test utils ~~~')
         self._TransformAndCopyFolder(
                 test_util_source_dir, test_util_dest_dir, transform_func=self._TransformTestCode)
+        for iface_file in (
+                'AppSearchSession.java', 'GlobalSearchSession.java', 'SearchResults.java'):
+            dest_file_name = os.path.splitext(iface_file)[0] + 'Shim.java'
+            self._TransformAndCopyFile(
+                    os.path.join(api_source_dir, 'app/' + iface_file),
+                    os.path.join(test_util_dest_dir, dest_file_name),
+                    transform_func=self._TransformTestCode,
+                    ignore_skips=True)
 
     def _ExportImplCode(self):
         impl_source_dir = os.path.join(self._jetpack_appsearch_root, JETPACK_IMPL_ROOT)
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 627e87a..1a20e58 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -47,6 +47,7 @@
 import com.google.android.icing.proto.IcingSearchEngineOptions;
 import com.google.android.icing.proto.InitializeResultProto;
 import com.google.android.icing.proto.OptimizeResultProto;
+import com.google.android.icing.proto.PersistToDiskResultProto;
 import com.google.android.icing.proto.PropertyConfigProto;
 import com.google.android.icing.proto.PropertyProto;
 import com.google.android.icing.proto.PutResultProto;
@@ -628,6 +629,25 @@
     }
 
     /**
+     * Persists all update/delete requests to the disk.
+     *
+     * <p>If the app crashes after a call to PersistToDisk(), Icing would be able to fully recover
+     * all data written up to this point without a costly recovery process.
+     *
+     * <p>If the app crashes before a call to PersistToDisk(), Icing would trigger a costly
+     * recovery process in next initialization. After that, Icing would still be able to recover
+     * all written data.
+     *
+     * @throws AppSearchException
+     */
+    public void persistToDisk() throws AppSearchException {
+        PersistToDiskResultProto persistToDiskResultProto =
+                mIcingSearchEngineLocked.persistToDisk();
+        checkSuccess(persistToDiskResultProto.getStatus());
+    }
+
+
+    /**
      * Clears documents and schema across all packages and databaseNames.
      *
      * <p>This method also clear all data in {@link VisibilityStore}, an
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index e06f75d..8f6cfde 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -175,9 +175,30 @@
     public static ListenableFuture<AppSearchSession> createSearchSession(
             @NonNull SearchContext context) {
         Preconditions.checkNotNull(context);
-        return FutureUtil.execute(EXECUTOR_SERVICE, () -> {
+        return createSearchSession(context, EXECUTOR_SERVICE);
+    }
+
+    /**
+     * Opens a new {@link AppSearchSession} on this storage with executor.
+     *
+     * <p>This process requires a native search library. If it's not created, the initialization
+     * process will create one.
+     *
+     * @param context  The {@link SearchContext} contains all information to create a new
+     *                 {@link AppSearchSession}
+     * @param executor The executor of where tasks will execute.
+     * @hide
+     */
+    @NonNull
+    @VisibleForTesting
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static ListenableFuture<AppSearchSession> createSearchSession(
+            @NonNull SearchContext context, @NonNull ExecutorService executor) {
+        Preconditions.checkNotNull(context);
+        Preconditions.checkNotNull(executor);
+        return FutureUtil.execute(executor, () -> {
             LocalStorage instance = getOrCreateInstance(context.mContext);
-            return instance.doCreateSearchSession(context);
+            return instance.doCreateSearchSession(context, executor);
         });
     }
 
@@ -197,7 +218,7 @@
         Preconditions.checkNotNull(context);
         return FutureUtil.execute(EXECUTOR_SERVICE, () -> {
             LocalStorage instance = getOrCreateInstance(context.mContext);
-            return instance.doCreateGlobalSearchSession();
+            return instance.doCreateGlobalSearchSession(EXECUTOR_SERVICE);
         });
     }
 
@@ -230,13 +251,14 @@
     }
 
     @NonNull
-    private AppSearchSession doCreateSearchSession(@NonNull SearchContext context) {
-        return new SearchSessionImpl(mAppSearchImpl, EXECUTOR_SERVICE,
+    private AppSearchSession doCreateSearchSession(@NonNull SearchContext context,
+            @NonNull ExecutorService executor) {
+        return new SearchSessionImpl(mAppSearchImpl, executor,
                 context.mContext.getPackageName(), context.mDatabaseName);
     }
 
     @NonNull
-    private GlobalSearchSession doCreateGlobalSearchSession() {
-        return new GlobalSearchSessionImpl(mAppSearchImpl, EXECUTOR_SERVICE);
+    private GlobalSearchSession doCreateGlobalSearchSession(@NonNull ExecutorService executor) {
+        return new GlobalSearchSessionImpl(mAppSearchImpl, executor);
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
index c846667..e11f1f0 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
@@ -53,6 +53,8 @@
 
     private boolean mIsFirstLoad = true;
 
+    private boolean mIsClosed = false;
+
     SearchResultsImpl(
             @NonNull AppSearchImpl appSearchImpl,
             @NonNull ExecutorService executorService,
@@ -71,6 +73,7 @@
     @Override
     @NonNull
     public ListenableFuture<List<SearchResult>> getNextPage() {
+        Preconditions.checkState(!mIsClosed, "SearchResults has already been closed");
         return FutureUtil.execute(mExecutorService, () -> {
             SearchResultPage searchResultPage;
             if (mIsFirstLoad) {
@@ -104,9 +107,12 @@
     public void close() {
         // Checking the future result is not needed here since this is a cleanup step which is not
         // critical to the correct functioning of the system; also, the return value is void.
-        FutureUtil.execute(mExecutorService, () -> {
-            mAppSearchImpl.invalidateNextPageToken(mNextPageToken);
-            return null;
-        });
+        if (!mIsClosed) {
+            FutureUtil.execute(mExecutorService, () -> {
+                mAppSearchImpl.invalidateNextPageToken(mNextPageToken);
+                mIsClosed = true;
+                return null;
+            });
+        }
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index 8bc6eb9..973b724 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -53,6 +53,8 @@
     private final ExecutorService mExecutorService;
     private final String mPackageName;
     private final String mDatabaseName;
+    private boolean mIsMutated = false;
+    private boolean mIsClosed = false;
 
     SearchSessionImpl(
             @NonNull AppSearchImpl appSearchImpl,
@@ -69,6 +71,7 @@
     @NonNull
     public ListenableFuture<Void> setSchema(@NonNull SetSchemaRequest request) {
         Preconditions.checkNotNull(request);
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
             mAppSearchImpl.setSchema(
                     mPackageName,
@@ -76,6 +79,7 @@
                     new ArrayList<>(request.getSchemas()),
                     new ArrayList<>(request.getSchemasNotVisibleToSystemUi()),
                     request.isForceOverride());
+            mIsMutated = true;
             return null;
         });
     }
@@ -83,6 +87,7 @@
     @Override
     @NonNull
     public ListenableFuture<Set<AppSearchSchema>> getSchema() {
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
             List<AppSearchSchema> schemas = mAppSearchImpl.getSchema(mPackageName, mDatabaseName);
             return new ArraySet<>(schemas);
@@ -94,6 +99,7 @@
     public ListenableFuture<AppSearchBatchResult<String, Void>> putDocuments(
             @NonNull PutDocumentsRequest request) {
         Preconditions.checkNotNull(request);
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
             AppSearchBatchResult.Builder<String, Void> resultBuilder =
                     new AppSearchBatchResult.Builder<>();
@@ -106,6 +112,7 @@
                     resultBuilder.setResult(document.getUri(), throwableToFailedResult(t));
                 }
             }
+            mIsMutated = true;
             return resultBuilder.build();
         });
     }
@@ -115,6 +122,7 @@
     public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByUri(
             @NonNull GetByUriRequest request) {
         Preconditions.checkNotNull(request);
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
             AppSearchBatchResult.Builder<String, GenericDocument> resultBuilder =
                     new AppSearchBatchResult.Builder<>();
@@ -140,6 +148,7 @@
             @NonNull SearchSpec searchSpec) {
         Preconditions.checkNotNull(queryExpression);
         Preconditions.checkNotNull(searchSpec);
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return new SearchResultsImpl(
                 mAppSearchImpl,
                 mExecutorService,
@@ -154,6 +163,7 @@
     public ListenableFuture<AppSearchBatchResult<String, Void>> removeByUri(
             @NonNull RemoveByUriRequest request) {
         Preconditions.checkNotNull(request);
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
             AppSearchBatchResult.Builder<String, Void> resultBuilder =
                     new AppSearchBatchResult.Builder<>();
@@ -165,6 +175,7 @@
                     resultBuilder.setResult(uri, throwableToFailedResult(t));
                 }
             }
+            mIsMutated = true;
             return resultBuilder.build();
         });
     }
@@ -175,12 +186,27 @@
             @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
         Preconditions.checkNotNull(queryExpression);
         Preconditions.checkNotNull(searchSpec);
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
             mAppSearchImpl.removeByQuery(mPackageName, mDatabaseName, queryExpression, searchSpec);
+            mIsMutated = true;
             return null;
         });
     }
 
+    @Override
+    @SuppressWarnings("FutureReturnValueIgnored")
+    public void close() {
+        if (mIsMutated && !mIsClosed) {
+            // No future is needed here since the method is void.
+            FutureUtil.execute(mExecutorService, () -> {
+                mAppSearchImpl.persistToDisk();
+                mIsClosed = true;
+                return null;
+            });
+        }
+    }
+
     private <T> ListenableFuture<T> execute(Callable<T> callable) {
         return FutureUtil.execute(mExecutorService, callable);
     }
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index 1e2c5ac..d3040c6 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -99,7 +99,7 @@
 const val REACTIVE_STREAMS = "org.reactivestreams:reactive-streams:1.0.0"
 const val RX_JAVA = "io.reactivex.rxjava2:rxjava:2.2.9"
 const val RX_JAVA3 = "io.reactivex.rxjava3:rxjava:3.0.0"
-val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.1.18"
+val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.1.21"
 val SKIKO = "org.jetbrains.skiko:skiko-jvm:$SKIKO_VERSION"
 val SKIKO_LINUX_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-linux-x64:$SKIKO_VERSION"
 val SKIKO_MACOS_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-macos-x64:$SKIKO_VERSION"
diff --git a/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
index 01510147..0954725 100644
--- a/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
@@ -210,21 +210,14 @@
         val docsRuntimeClasspath = project.configurations.create("docs-runtime-classpath") {
             it.setResolveClasspathForUsage(Usage.JAVA_RUNTIME)
         }
-        docsCompileClasspath.resolutionStrategy {
-            val buildVersions = (project.rootProject.property("ext") as ExtraPropertiesExtension)
-                .let { it.get("build_versions") as Map<*, *> }
-            it.eachDependency { details ->
-                if (details.requested.group == "org.jetbrains.kotlin") {
-                    details.useVersion(buildVersions["kotlin"] as String)
-                }
-            }
-        }
-        docsRuntimeClasspath.resolutionStrategy {
-            val buildVersions = (project.rootProject.property("ext") as ExtraPropertiesExtension)
-                .let { it.get("build_versions") as Map<*, *> }
-            it.eachDependency { details ->
-                if (details.requested.group == "org.jetbrains.kotlin") {
-                    details.useVersion(buildVersions["kotlin"] as String)
+        listOf(docsCompileClasspath, docsRuntimeClasspath).forEach { config ->
+            config.resolutionStrategy {
+                val versions = (project.rootProject.property("ext") as ExtraPropertiesExtension)
+                    .let { it.get("build_versions") as Map<*, *> }
+                it.eachDependency { details ->
+                    if (details.requested.group == "org.jetbrains.kotlin") {
+                        details.useVersion(versions["kotlin"] as String)
+                    }
                 }
             }
         }
diff --git a/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt b/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
index c92d780..049dc31 100644
--- a/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
@@ -128,6 +128,9 @@
     "dokkaKotlinDocs",
     "zipDokkaDocs",
 
+    // Flakily not up-to-date, b/176120659
+    "doclavaDocs",
+
     // We should be able to remove these entries when b/160392650 is fixed
     "lint",
     "lintDebug",
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
index 628cc89..c2e6423 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -20,7 +20,7 @@
 import android.graphics.Rect
 import android.hardware.camera2.CameraCharacteristics
 import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log.warn
+import androidx.camera.camera2.pipe.core.Log.warn
 import androidx.camera.camera2.pipe.integration.config.CameraScope
 import androidx.camera.camera2.pipe.integration.impl.CameraProperties
 import androidx.camera.camera2.pipe.integration.impl.EvCompControl
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
index 120f8b9..65f755f 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
@@ -17,11 +17,11 @@
 
 import android.content.Context
 import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.impl.Debug
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Timestamps
-import androidx.camera.camera2.pipe.impl.Timestamps.measureNow
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log.debug
+import androidx.camera.camera2.pipe.core.Timestamps
+import androidx.camera.camera2.pipe.core.Timestamps.measureNow
+import androidx.camera.camera2.pipe.core.Timestamps.formatMs
 import androidx.camera.camera2.pipe.integration.config.CameraConfig
 import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
 import androidx.camera.camera2.pipe.integration.config.CameraAppConfig
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 6c6d6da..0bf359a 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -20,7 +20,7 @@
 import android.hardware.camera2.CameraCharacteristics
 import android.view.Surface
 import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.core.Log
 import androidx.camera.camera2.pipe.integration.config.CameraConfig
 import androidx.camera.camera2.pipe.integration.config.CameraScope
 import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
index d3dcff7..7e753b2 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
@@ -17,8 +17,8 @@
 package androidx.camera.camera2.pipe.integration.adapter
 
 import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Log.warn
+import androidx.camera.camera2.pipe.core.Log.debug
+import androidx.camera.camera2.pipe.core.Log.warn
 import androidx.camera.camera2.pipe.integration.config.CameraConfig
 import androidx.camera.camera2.pipe.integration.config.CameraScope
 import androidx.camera.camera2.pipe.integration.impl.UseCaseManager
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
index d026e65..4b9e616 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
@@ -19,7 +19,7 @@
 import android.graphics.ImageFormat
 import android.util.Size
 import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.core.Log.debug
 import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
 import androidx.camera.core.impl.CameraDeviceSurfaceManager
 import androidx.camera.core.impl.SurfaceConfig
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
index dcbcba5..a276b2c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
@@ -22,8 +22,8 @@
 import android.util.Size
 import android.view.Display
 import android.view.WindowManager
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Log.info
+import androidx.camera.camera2.pipe.core.Log.debug
+import androidx.camera.camera2.pipe.core.Log.info
 import androidx.camera.camera2.pipe.integration.impl.asLandscape
 import androidx.camera.camera2.pipe.integration.impl.minByArea
 import androidx.camera.camera2.pipe.integration.impl.toSize
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
index 63f471f..0da56f5 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
@@ -21,7 +21,7 @@
 import androidx.camera.camera2.pipe.FrameInfo
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.RequestMetadata
-import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.core.Log
 import androidx.camera.camera2.pipe.integration.impl.CAMERAX_TAG_BUNDLE
 import androidx.camera.core.impl.CameraCaptureMetaData.AeState
 import androidx.camera.core.impl.CameraCaptureMetaData.AfMode
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
index 93ff9e7..082989e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
@@ -16,18 +16,15 @@
 
 package androidx.camera.camera2.pipe.integration.impl
 
-import android.hardware.camera2.CameraDevice
 import android.hardware.camera2.CaptureRequest
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.FrameNumber
-import androidx.camera.camera2.pipe.RequestTemplate
-import androidx.camera.camera2.pipe.StreamConfig
+import androidx.camera.camera2.pipe.CameraStream
+import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.StreamFormat
 import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.StreamType
 import androidx.camera.camera2.pipe.TorchState
-import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.core.Log.debug
 import androidx.camera.camera2.pipe.integration.config.CameraConfig
 import androidx.camera.camera2.pipe.integration.config.UseCaseCameraScope
 import androidx.camera.core.UseCase
@@ -51,7 +48,7 @@
     fun <T> setParametersAsync(values: Map<CaptureRequest.Key<*>, Any>): Deferred<Unit>
 
     // 3A
-    suspend fun setTorchAsync(enabled: Boolean): Deferred<FrameNumber>
+    suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A>
 
     // Capture
 
@@ -90,7 +87,7 @@
         cameraGraph.close()
     }
 
-    override suspend fun setTorchAsync(enabled: Boolean): Deferred<FrameNumber> {
+    override suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A> {
         return cameraGraph.acquireSession().use {
             it.setTorch(
                 when (enabled) {
@@ -152,31 +149,28 @@
                 callbackMap: CameraCallbackMap,
                 coroutineScope: CoroutineScope,
             ): UseCaseCamera {
-                val streamConfigs = mutableListOf<StreamConfig>()
-                val useCaseMap = mutableMapOf<StreamConfig, UseCase>()
+                val streamConfigs = mutableListOf<CameraStream.Config>()
+                val useCaseMap = mutableMapOf<CameraStream.Config, UseCase>()
 
                 // TODO: This may need to combine outputs that are (or will) share the same output
                 //  imageReader or surface. Right now, each UseCase gets its own [StreamConfig]
                 // TODO: useCases only have a single `attachedSurfaceResolution`, yet they have a
                 //  list of deferrableSurfaces.
                 for (useCase in useCases) {
-                    val config = StreamConfig(
+                    val outputConfig = CameraStream.Config.create(
                         size = useCase.attachedSurfaceResolution!!,
                         format = StreamFormat(useCase.imageFormat),
-                        camera = cameraConfig.cameraId,
-                        type = StreamType.SURFACE,
-                        deferrable = false
+                        camera = cameraConfig.cameraId
                     )
-                    streamConfigs.add(config)
-                    useCaseMap[config] = useCase
+                    streamConfigs.add(outputConfig)
+                    useCaseMap[outputConfig] = useCase
                 }
 
                 // Build up a config (using TEMPLATE_PREVIEW by default)
                 val config = CameraGraph.Config(
                     camera = cameraConfig.cameraId,
                     streams = streamConfigs,
-                    listeners = listOf(callbackMap),
-                    template = RequestTemplate(CameraDevice.TEMPLATE_PREVIEW)
+                    defaultListeners = listOf(callbackMap),
                 )
                 val graph = cameraPipe.create(config)
 
@@ -189,7 +183,7 @@
                     //  this code assumes only a single surface per UseCase.
                     val deferredSurfaces = useCaseSessionConfig?.surfaces
                     if (stream != null && deferredSurfaces != null && deferredSurfaces.size == 1) {
-                        val deferredSurface = deferredSurfaces[0]
+                        val deferredSurface = deferredSurfaces.first()
                         graph.setSurface(stream.id, deferredSurface.surface.get())
                         surfaceToStreamMap[deferredSurface] = stream.id
                     }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt
index 09607b0..90f9b42 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt
@@ -166,8 +166,8 @@
                     request = Request(
                         template = currentTemplate,
                         streams = currentStreams.toList(),
-                        requestParameters = currentParameters.toMap(),
-                        extraRequestParameters = currentInternalParameters.toMap(),
+                        parameters = currentParameters.toMap(),
+                        extras = currentInternalParameters.toMap()
                     )
                     result = updateSignal
                     updateSignal = null
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 1b76c6ff..2071570 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -16,7 +16,7 @@
 
 package androidx.camera.camera2.pipe.integration.impl
 
-import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.core.Log
 import androidx.camera.camera2.pipe.integration.config.CameraConfig
 import androidx.camera.camera2.pipe.integration.config.CameraScope
 import androidx.camera.camera2.pipe.integration.config.UseCaseCameraComponent
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeMetadata.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeMetadata.kt
index 1e8c988..bb9e816 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeMetadata.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeMetadata.kt
@@ -97,13 +97,13 @@
  */
 public class FakeRequestMetadata(
     private val requestParameters: Map<CaptureRequest.Key<*>, Any?> = emptyMap(),
-    extraRequestParameters: Map<Metadata.Key<*>, Any?> = emptyMap(),
+    metadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
     override val template: RequestTemplate = RequestTemplate(0),
     override val streams: Map<StreamId, Surface> = mapOf(),
     override val repeating: Boolean = false,
     override val request: Request = Request(listOf()),
     override val requestNumber: RequestNumber = RequestNumber(4321)
-) : FakeMetadata(request.extraRequestParameters.plus(extraRequestParameters)), RequestMetadata {
+) : FakeMetadata(request.extras.plus(metadata)), RequestMetadata {
 
     override fun <T> get(key: CaptureRequest.Key<T>): T? = requestParameters[key] as T?
     override fun <T> getOrDefault(key: CaptureRequest.Key<T>, default: T): T = get(key) ?: default
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
index 06a49c1..4333589 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
@@ -76,8 +76,8 @@
             requestParameters = mapOf(CaptureRequest.JPEG_QUALITY to 95),
             request = Request(
                 streams = listOf(),
-                requestParameters = mapOf(CaptureRequest.JPEG_QUALITY to 20),
-                extraRequestParameters = mapOf(FakeMetadata.TEST_KEY to 42)
+                parameters = mapOf(CaptureRequest.JPEG_QUALITY to 20),
+                extras = mapOf(FakeMetadata.TEST_KEY to 42)
             )
         )
 
@@ -124,8 +124,8 @@
         requestParameters = mapOf(CaptureRequest.JPEG_QUALITY to 95),
         request = Request(
             streams = listOf(),
-            requestParameters = mapOf(CaptureRequest.JPEG_QUALITY to 20),
-            extraRequestParameters = mapOf(FakeMetadata.TEST_KEY to 42)
+            parameters = mapOf(CaptureRequest.JPEG_QUALITY to 20),
+            extras = mapOf(FakeMetadata.TEST_KEY to 42)
         )
     )
 
diff --git a/camera/camera-camera2-pipe/lint-baseline.xml b/camera/camera-camera2-pipe/lint-baseline.xml
deleted file mode 100644
index c8dcd32..0000000
--- a/camera/camera-camera2-pipe/lint-baseline.xml
+++ /dev/null
@@ -1,114 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-alpha15" client="gradle" variant="debug" version="4.2.0-alpha15">
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 23, the call containing class androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice is not annotated with @RequiresApi(x) where x is at least 23. Either annotate the containing class with at least @RequiresApi(23) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(23)."
-        errorLine1="        cameraDevice.createReprocessableCaptureSession("
-        errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt"
-            line="159"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 23, the call containing class androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice is not annotated with @RequiresApi(x) where x is at least 23. Either annotate the containing class with at least @RequiresApi(23) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(23)."
-        errorLine1="        cameraDevice.createConstrainedHighSpeedCaptureSession("
-        errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt"
-            line="177"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 24, the call containing class androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice is not annotated with @RequiresApi(x) where x is at least 24. Either annotate the containing class with at least @RequiresApi(24) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(24)."
-        errorLine1="        cameraDevice.createCaptureSessionByOutputConfigurations("
-        errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt"
-            line="194"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 24, the call containing class androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice is not annotated with @RequiresApi(x) where x is at least 24. Either annotate the containing class with at least @RequiresApi(24) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(24)."
-        errorLine1="        cameraDevice.createReprocessableCaptureSessionByConfigurations("
-        errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt"
-            line="212"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 23, the call containing class androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice is not annotated with @RequiresApi(x) where x is at least 23. Either annotate the containing class with at least @RequiresApi(23) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(23)."
-        errorLine1="            InputConfiguration(inputConfig.width, inputConfig.height, inputConfig.format),"
-        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt"
-            line="213"
-            column="13"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 28, the call containing class androidx.camera.camera2.pipe.impl.CameraMetadataImpl is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
-        errorLine1="                        characteristics.physicalCameraIds.orEmpty().map { CameraId(it) }.toSet()"
-        errorLine2="                                        ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt"
-            line="108"
-            column="41"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 28, the call containing class androidx.camera.camera2.pipe.impl.CameraMetadataImpl is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
-        errorLine1="                        characteristics.availablePhysicalCameraRequestKeys.orEmpty().toSet()"
-        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt"
-            line="124"
-            column="41"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 28, the call containing class androidx.camera.camera2.pipe.impl.CameraMetadataImpl is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
-        errorLine1="                        characteristics.availableSessionKeys.orEmpty().toSet()"
-        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt"
-            line="140"
-            column="41"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 28, the call containing class androidx.camera.camera2.pipe.impl.AndroidFrameInfo is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
-        errorLine1="            val physicalResults = totalCaptureResult.physicalCameraResults"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/camera2/pipe/impl/FrameMetadata.kt"
-            line="105"
-            column="54"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
-        message="This call is to a method from API 28, the call containing class androidx.camera.camera2.pipe.impl.VirtualCameraManager is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
-        errorLine1="                        instance.openCamera("
-        errorLine2="                                 ~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/camera2/pipe/impl/VirtualCameraManager.kt"
-            line="228"
-            column="34"/>
-    </issue>
-
-</issues>
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Cameras.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt
similarity index 69%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Cameras.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt
index 4fc0990..cd2f1aa 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Cameras.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt
@@ -14,12 +14,14 @@
  * limitations under the License.
  */
 
+@file:Suppress("NOTHING_TO_INLINE")
+
 package androidx.camera.camera2.pipe
 
 /**
  * Methods for querying, iterating, and selecting the Cameras that are available on the device.
  */
-public interface Cameras {
+public interface CameraDevices {
     /**
      * Iterate and return a list of CameraId's on the device that are capable of being opened. Some
      * camera devices may be hidden or un-openable if they are included as part of a logical camera
@@ -39,4 +41,20 @@
      * available.
      */
     public fun awaitMetadata(camera: CameraId): CameraMetadata
+}
+
+@Suppress("EXPERIMENTAL_FEATURE_WARNING")
+public inline class CameraId(public val value: String) {
+    public companion object {
+        public inline fun fromCamera2Id(value: String): CameraId = CameraId(value)
+        public inline fun fromCamera1Id(value: Int): CameraId = CameraId("$value")
+    }
+
+    /**
+     * Attempt to parse an camera1 id from a camera2 id.
+     *
+     * @return The parsed Camera1 id, or null if the value cannot be parsed as a Camera1 id.
+     */
+    public inline fun toCamera1Id(): Int? = value.toIntOrNull()
+    public override fun toString(): String = "Camera $value"
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index 10e7b5c..865e8c0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.camera2.pipe
 
+import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.params.MeteringRectangle
 import android.view.Surface
 import kotlinx.coroutines.Deferred
@@ -25,7 +26,7 @@
  * A CameraGraph represents the combined configuration and state of a camera.
  */
 public interface CameraGraph : Closeable {
-    public val streams: Map<StreamConfig, Stream>
+    public val streams: StreamGraph
 
     /**
      * This will cause the CameraGraph to start opening the camera and configuring the Camera2
@@ -61,28 +62,23 @@
     public fun setSurface(stream: StreamId, surface: Surface?)
 
     /**
-     * This defines the configuration, flags, and pre-defined behavior of a CameraGraph instance.
+     * This defines the configuration, flags, and pre-defined structure of a CameraGraph instance.
      */
     public data class Config(
         val camera: CameraId,
-        val streams: List<StreamConfig>,
-        val template: RequestTemplate,
-        val defaultParameters: Map<Any?, Any> = emptyMap(),
-        val inputStream: InputStreamConfig? = null,
-        val operatingMode: OperatingMode = OperatingMode.NORMAL,
-        val listeners: List<Request.Listener> = listOf(),
+        val streams: List<CameraStream.Config>,
+        val streamSharingGroups: List<List<CameraStream.Config>> = listOf(),
+        val input: InputStream.Config? = null,
+        val sessionTemplate: RequestTemplate = RequestTemplate(1),
+        val sessionParameters: Map<CaptureRequest.Key<*>, Any> = emptyMap(),
+        val sessionMode: OperatingMode = OperatingMode.NORMAL,
+        val defaultTemplate: RequestTemplate = RequestTemplate(1),
+        val defaultParameters: Map<*, Any> = emptyMap<Any, Any>(),
+        val defaultListeners: List<Request.Listener> = listOf(),
         val metadataTransform: MetadataTransform = MetadataTransform(),
         val flags: Flags = Flags()
-    )
 
-    /**
-     * Configuration for defining the properties of a Camera2 InputStream for reprocessing
-     * requests.
-     */
-    public data class InputStreamConfig(
-        val width: Int,
-        val height: Int,
-        val format: Int
+        // TODO: Internal error handling. May be better at the CameraPipe level.
     )
 
     /**
@@ -171,10 +167,18 @@
         /**
          * Turns the torch to ON or OFF.
          *
+         * This method has a side effect on the currently set AE mode. Ref:
+         * https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#FLASH_MODE
+         * To use the flash control, AE mode must be set to ON or OFF. So if the AE mode is
+         * already not either ON or OFF, we will need to update the AE mode to one of those states,
+         * here we will choose ON. It is the responsibility of the application layer above
+         * CameraPipe to restore the AE mode after the torch control has been used. The
+         * [update3A] method can be used to restore the AE state to a previous value.
+         *
          * @return the FrameNumber at which the turn was fully turned on if switch was ON, or the
          * FrameNumber at which it was completely turned off when the switch was OFF.
          */
-        public fun setTorch(torchState: TorchState): Deferred<FrameNumber>
+        public fun setTorch(torchState: TorchState): Deferred<Result3A>
 
         /**
          * Locks the auto-exposure, auto-focus and auto-whitebalance as per the given desired
@@ -248,7 +252,7 @@
          *
          * @param frameLimit the maximum number of frames to wait before we give up waiting for
          * this operation to complete.
-         * @param timeLimitMs the maximum time limit in ms we wait before we give up waiting for
+         * @param timeLimitNs the maximum time limit in ms we wait before we give up waiting for
          * this operation to complete.
          *
          * @return [Result3A], which will contain the latest frame number at which the locks were
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraId.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraId.kt
deleted file mode 100644
index 4fcb4cb..0000000
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraId.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 2020 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.
- */
-
-@file:Suppress("NOTHING_TO_INLINE")
-
-package androidx.camera.camera2.pipe
-
-@Suppress("EXPERIMENTAL_FEATURE_WARNING")
-public inline class CameraId(public val value: String) {
-    public companion object {
-        public inline fun fromCamera2Id(value: String): CameraId = CameraId(value)
-        public inline fun fromCamera1Id(value: Int): CameraId = CameraId("$value")
-    }
-
-    /**
-     * Attempt to parse an camera1 id from a camera2 id.
-     *
-     * @return The parsed Camera1 id, or null if the value cannot be parsed as a Camera1 id.
-     */
-    public inline fun toCamera1Id(): Int? = value.toIntOrNull()
-    public override fun toString(): String = "Camera $value"
-}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
index 4c4b458..2668286 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
@@ -55,7 +55,7 @@
     /**
      * This provides access to information about the available cameras on the device.
      */
-    public fun cameras(): Cameras {
+    public fun cameras(): CameraDevices {
         return component.cameras()
     }
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/FlashMode.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/FlashMode.kt
new file mode 100644
index 0000000..5fac6d3
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/FlashMode.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2020 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.camera.camera2.pipe
+
+import android.hardware.camera2.CameraMetadata
+
+/**
+ * An enum to match the CameraMetadata.FLASH_MODE_* constants.
+ */
+public enum class FlashMode(public val value: Int) {
+    OFF(CameraMetadata.FLASH_MODE_OFF),
+    SINGLE(CameraMetadata.FLASH_MODE_SINGLE),
+    TORCH(CameraMetadata.FLASH_MODE_TORCH);
+
+    public companion object {
+        @JvmStatic
+        public fun fromIntOrNull(value: Int): FlashMode? = values().firstOrNull {
+            it.value == value
+        }
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
index ef80195..5272007 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
@@ -243,7 +243,7 @@
 /**
  * Utility function to help deal with the unsafe nature of the typed Key/Value pairs.
  */
-public fun CaptureRequest.Builder.writeParameter(key: Any?, value: Any?) {
+public fun CaptureRequest.Builder.writeParameter(key: Any?, value: Any) {
     if (key != null && key is CaptureRequest.Key<*>) {
         @Suppress("UNCHECKED_CAST")
         this.set(key as CaptureRequest.Key<Any>, value)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt
index 79ed2c0..0e150b0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt
@@ -27,8 +27,8 @@
  */
 public data class Request(
     val streams: List<StreamId>,
-    val requestParameters: Map<CaptureRequest.Key<*>, Any> = emptyMap(),
-    val extraRequestParameters: Map<Metadata.Key<*>, Any> = emptyMap(),
+    val parameters: Map<CaptureRequest.Key<*>, Any> = emptyMap(),
+    val extras: Map<Metadata.Key<*>, Any> = emptyMap(),
     val listeners: List<Listener> = emptyList(),
     val template: RequestTemplate? = null
 ) {
@@ -212,11 +212,11 @@
 
     @Suppress("UNCHECKED_CAST")
     private fun <T> Request.getUnchecked(key: Metadata.Key<T>): T? =
-        this.extraRequestParameters[key] as T?
+        this.extras[key] as T?
 
     @Suppress("UNCHECKED_CAST")
     private fun <T> Request.getUnchecked(key: CaptureRequest.Key<T>): T? =
-        this.requestParameters[key] as T?
+        this.parameters[key] as T?
 }
 
 public fun <T> Request.getOrDefault(key: Metadata.Key<T>, default: T): T = this[key] ?: default
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Stream.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Stream.kt
deleted file mode 100644
index c40476d..0000000
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Stream.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2020 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.camera.camera2.pipe
-
-import android.util.Size
-
-@Suppress("EXPERIMENTAL_FEATURE_WARNING")
-public inline class StreamId(public val value: Int) {
-    override fun toString(): String = "Stream-$value"
-}
-
-/**
- * A Stream is an identifier for a specific stream as well as the properties that were used to
- * create this stream instance. This allows StreamConfig's to be used to build multiple
- * [CameraGraph]'s  while enforcing that each [Stream] will only work with that specific camera
- * [CameraGraph] instance.
- */
-public interface Stream {
-    public val id: StreamId
-    public val size: Size
-    public val format: StreamFormat
-    public val camera: CameraId
-    public val type: StreamType
-}
-
-/**
- * Configuration object that provides the parameters for a specific input / output stream on Camera.
- */
-public class StreamConfig(
-    public val size: Size,
-    public val format: StreamFormat,
-    public val camera: CameraId,
-    public val type: StreamType,
-    public val deferrable: Boolean = true
-)
-
-/**
- * Camera2 allows the camera to be configured with outputs that are not immediately available.
- * This allows the camera to configure the internal pipeline with additional information about
- * the surface that has not yet been provided to the camera.
- */
-public enum class StreamType {
-    SURFACE,
-    SURFACE_VIEW,
-    SURFACE_TEXTURE
-}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt
index 9b55e47..c8f0bf2 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt
@@ -21,6 +21,7 @@
  *
  * Using this inline class prevents missing values on platforms where the format is not present
  * or not listed.
+ * // TODO: Consider adding data-space as a separate property, or finding a way to work it in.
  */
 @Suppress("EXPERIMENTAL_FEATURE_WARNING")
 public inline class StreamFormat(public val value: Int) {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamGraph.kt
new file mode 100644
index 0000000..a161543
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamGraph.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020 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.camera.camera2.pipe
+
+/**
+ * This defines a fixed set of inputs and outputs for a single [CameraGraph] instance.
+ *
+ * [CameraStream]s can be used to build [Request]s that are sent to a [CameraGraph].
+ */
+public interface StreamGraph {
+    public val streams: List<CameraStream>
+    public val input: InputStream?
+    public val outputs: List<OutputStream>
+
+    public operator fun get(config: CameraStream.Config): CameraStream?
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
new file mode 100644
index 0000000..663ae3f
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2020 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.camera.camera2.pipe
+
+import android.hardware.camera2.params.OutputConfiguration
+import android.util.Size
+
+/**
+ * A [CameraStream] is used on a [CameraGraph] to control what outputs that graph produces.
+ *
+ * - Each [CameraStream] must have a surface associated with it in the [CameraGraph]. This surface
+ *   may be changed, although this may cause the camera to stall and reconfigure.
+ * - [CameraStream]'s may be added to [Request]'s that are sent to the [CameraGraph]. This causes
+ *   the associated surface to be used by the Camera to produce one or more of the outputs (defined
+ *   by outputs.
+ *
+ * [CameraStream] may be configured in several different ways with the requirement that each
+ * [CameraStream] may only represent a single surface that is sent to Camera2, and that each
+ * [CameraStream] must produce one or more distinct outputs.
+ *
+ * There are three main components that will be wired together, the [CameraStream], the
+ * Camera2 [OutputConfiguration], and the [OutputStream]'s. In each of these examples a
+ * [CameraStream] is associated with a distinct surface that may be sent to camera2 to produce 1
+ * or more distinct outputs defined in the list of [OutputStream]'s.
+ *
+ * Simple 1:1 configuration
+ *   ```
+ *   CameraStream-1 -> OutputConfig-1 -> OutputStream-1
+ *   CameraStream-2 -> OutputConfig-2 -> OutputStream-2
+ *   ```
+ *
+ * Stream sharing (Multiple surfaces use the same OutputConfig object)
+ *   ```
+ *   CameraStream-1 --------------------> OutputStream-1
+ *                  >- OutputConfig-1 -<
+ *   CameraStream-2 --------------------> OutputStream-2
+ *   ```
+ *
+ * Multi-Output / External OutputConfiguration (Camera2 may produce one or more of the outputs)
+ *   ```
+ *   CameraStream-1 -> OutputConfig-1 -> OutputStream-1
+ *                 \-> OutputConfig-2 -> OutputStream-2
+ *   ```
+ */
+public class CameraStream internal constructor(
+    public val id: StreamId,
+    public val outputs: List<OutputStream>
+) {
+    override fun toString(): String = id.toString()
+
+    /** Configuration that may be used to define a [CameraStream] on a [CameraGraph] */
+    public class Config internal constructor(
+        val outputs: List<OutputStream.Config>
+    ) {
+        companion object {
+            /** Create a simple [CameraStream] to [OutputStream] configuration */
+            fun create(
+                size: Size,
+                format: StreamFormat,
+                camera: CameraId? = null,
+                outputType: OutputStream.OutputType = OutputStream.OutputType.SURFACE
+            ): Config = create(
+                OutputStream.Config.create(size, format, camera, outputType)
+            )
+
+            /**
+             * Create a simple [CameraStream] using a previously defined [OutputStream.Config].
+             * This allows multiple [CameraStream]s to share the same [OutputConfiguration].
+             */
+            fun create(output: OutputStream.Config) = Config(listOf(output))
+
+            /**
+             * Create a [CameraStream] from multiple [OutputStream.Config]s. This is used to to
+             * define a [CameraStream] that may produce one or more of the outputs when used in a
+             * request to the camera.
+             */
+            fun create(outputs: List<OutputStream.Config>) = Config(outputs)
+        }
+    }
+}
+
+/**
+ * This identifies a single surface that is used to tell the camera to produce one or more outputs.
+ */
+@Suppress("EXPERIMENTAL_FEATURE_WARNING")
+public inline class StreamId(public val value: Int) {
+    override fun toString(): String = "Stream-$value"
+}
+
+/**
+ * A [OutputStream] represents one of the possible outputs that may be produced from a
+ * [CameraStream]. Because some sensors are capable of producing images at different resolutions,
+ * the underlying HAL on the device may produce different sized images for the same request.
+ * This represents one of those potential outputs.
+ */
+public interface OutputStream {
+    // Every output comes from one, and exactly one, CameraStream
+    public val stream: CameraStream
+
+    public val id: OutputId
+    public val size: Size
+    public val format: StreamFormat
+    public val camera: CameraId
+    // TODO: Consider adding sensor mode and/or other metadata
+
+    /**
+     * Configuration object that provides the parameters for a specific input / output stream on Camera.
+     */
+    sealed class Config(
+        public val size: Size,
+        public val format: StreamFormat,
+        public val camera: CameraId?
+    ) {
+        companion object {
+            fun create(
+                size: Size,
+                format: StreamFormat,
+                camera: CameraId? = null,
+                outputType: OutputType = OutputType.SURFACE,
+                externalOutputConfig: OutputConfiguration? = null
+            ): Config =
+                if (externalOutputConfig != null) {
+                    ExternalOutputConfig(size, format, camera, output = externalOutputConfig)
+                } else if (
+                    outputType == OutputType.SURFACE_TEXTURE ||
+                    outputType == OutputType.SURFACE_VIEW
+                ) {
+                    LazyOutputConfig(size, format, camera, outputType)
+                } else {
+                    SimpleOutputConfig(size, format, camera)
+                }
+        }
+
+        /**
+         * Most outputs only need to define size, format, and cameraId.
+         */
+        internal class SimpleOutputConfig(
+            size: Size,
+            format: StreamFormat,
+            camera: CameraId?
+        ) : Config(size, format, camera)
+
+        /**
+         * Used to configure an output with a surface that may be provided after the camera is running.
+         *
+         * This behavior is allowed on newer versions of the OS and allows the camera to start
+         * running before the UI is fully available. This configuration mode is only allowed for
+         * SurfaceHolder and SurfaceTexture output targets, and must be defined ahead of time (along
+         * with the size, and format) for these [OutputConfiguration]'s to be created.
+         */
+        internal class LazyOutputConfig(
+            size: Size,
+            format: StreamFormat,
+            camera: CameraId?,
+            internal val outputType: OutputType
+        ) : Config(size, format, camera)
+
+        /**
+         * Used to define an output that comes from an externally managed OutputConfiguration object.
+         *
+         * The configuration logic has the following behavior:
+         * - Assumes [OutputConfiguration] has a valid surface
+         * - Assumes [OutputConfiguration] surfaces will not be added / removed / changed.
+         * - If the CameraCaptureSession must be recreated, the [OutputConfiguration] will be reused.
+         */
+        internal class ExternalOutputConfig(
+            size: Size,
+            format: StreamFormat,
+            camera: CameraId?,
+            val output: OutputConfiguration,
+        ) : Config(size, format, camera)
+    }
+
+    enum class OutputType {
+        SURFACE,
+        SURFACE_VIEW,
+        SURFACE_TEXTURE,
+    }
+}
+
+/**
+ * This identifies a single output.
+ */
+@Suppress("EXPERIMENTAL_FEATURE_WARNING")
+public inline class OutputId(public val value: Int) {
+    override fun toString(): String = "Output-$value"
+}
+
+/**
+ * Configuration for defining the properties of a Camera2 InputStream for reprocessing
+ * requests.
+ */
+public interface InputStream {
+    public val id: InputId
+    public val format: StreamFormat
+    // TODO: This may accept
+
+    public class Config(val stream: CameraStream.Config)
+}
+
+/**
+ * This identifies a single input.
+ */
+@Suppress("EXPERIMENTAL_FEATURE_WARNING")
+public inline class InputId(public val value: Int) {
+    override fun toString(): String = "Input-$value"
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Debug.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
similarity index 64%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Debug.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
index 4f219ea..6852aa6 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Debug.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
@@ -16,7 +16,7 @@
 
 @file:Suppress("NOTHING_TO_INLINE")
 
-package androidx.camera.camera2.pipe.impl
+package androidx.camera.camera2.pipe.core
 
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraCharacteristics.LENS_FACING
@@ -69,66 +69,68 @@
         }
     }
 
-    internal fun logConfiguration(
-        graphId: String,
+    public fun formatCameraGraphProperties(
         metadata: CameraMetadata,
         graphConfig: CameraGraph.Config,
-        streamMap: StreamMap
-    ) {
-        Log.info {
-            val lensFacing = when (metadata[LENS_FACING]) {
-                CameraCharacteristics.LENS_FACING_FRONT -> "Front"
-                CameraCharacteristics.LENS_FACING_BACK -> "Back"
-                CameraCharacteristics.LENS_FACING_EXTERNAL -> "External"
-                else -> "Unknown"
-            }
+        cameraGraph: CameraGraph
+    ): String {
+        val lensFacing = when (metadata[LENS_FACING]) {
+            CameraCharacteristics.LENS_FACING_FRONT -> "Front"
+            CameraCharacteristics.LENS_FACING_BACK -> "Back"
+            CameraCharacteristics.LENS_FACING_EXTERNAL -> "External"
+            else -> "Unknown"
+        }
 
-            val operatingMode = when (graphConfig.operatingMode) {
-                CameraGraph.OperatingMode.HIGH_SPEED -> "High Speed"
-                CameraGraph.OperatingMode.NORMAL -> "Normal"
-            }
+        val operatingMode = when (graphConfig.sessionMode) {
+            CameraGraph.OperatingMode.HIGH_SPEED -> "High Speed"
+            CameraGraph.OperatingMode.NORMAL -> "Normal"
+        }
 
-            val capabilities = metadata[REQUEST_AVAILABLE_CAPABILITIES]
-            val cameraType = if (capabilities != null &&
-                capabilities.contains(
-                        REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
-                    )
-            ) {
-                "Logical"
-            } else {
-                "Physical"
-            }
+        val capabilities = metadata[REQUEST_AVAILABLE_CAPABILITIES]
+        val cameraType = if (capabilities != null &&
+            capabilities.contains(REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)
+        ) {
+            "Logical"
+        } else {
+            "Physical"
+        }
 
-            StringBuilder().apply {
-                append("$graphId (Camera ${graphConfig.camera.value})\n")
-                append("  Facing:    $lensFacing ($cameraType)\n")
-                append("  Mode:      $operatingMode\n")
-                append("Streams:\n")
-                for (stream in streamMap.streamConfigMap) {
+        return StringBuilder().apply {
+            append("$cameraGraph (Camera ${graphConfig.camera.value})\n")
+            append("  Facing:    $lensFacing ($cameraType)\n")
+            append("  Mode:      $operatingMode\n")
+            append("Outputs:\n")
+            for (stream in cameraGraph.streams.streams) {
+                stream.outputs.forEachIndexed { i, output ->
                     append("  ")
-                    append(stream.value.id.toString().padEnd(12, ' '))
-                    append(stream.value.size.toString().padEnd(12, ' '))
-                    append(stream.value.format.name.padEnd(16, ' '))
-                    append(stream.value.type.toString().padEnd(16, ' '))
+                    val streamId = if (i == 0) output.stream.id.toString() else ""
+                    append(streamId.padEnd(10, ' '))
+                    append(output.id.toString().padEnd(10, ' '))
+                    append(output.size.toString().padEnd(12, ' '))
+                    append(output.format.name.padEnd(16, ' '))
+                    if (output.camera != graphConfig.camera) {
+                        append(" [")
+                        append(output.camera)
+                        append("]")
+                    }
                     append("\n")
                 }
+            }
 
-                if (graphConfig.defaultParameters.isEmpty()) {
-                    append("Default Parameters: (None)")
-                } else {
-                    append("Default Parameters:\n")
-                    for (
-                        parameter in graphConfig.defaultParameters.filter {
-                            it is CaptureRequest.Key<*>
-                        }
-                    ) {
-                        append("  ")
-                        append((parameter.key as CaptureRequest.Key<*>).name.padEnd(50, ' '))
-                        append(parameter.value)
-                    }
+            if (graphConfig.defaultParameters.isEmpty()) {
+                append("Session Parameters: (None)")
+            } else {
+                append("Session Parameters:\n")
+                val captureRequestParameters = graphConfig.sessionParameters.filter {
+                    it is CaptureRequest.Key<*>
                 }
-            }.toString()
-        }
+                for (parameter in captureRequestParameters) {
+                    append("  ")
+                    append((parameter.key).name.padEnd(50, ' '))
+                    append(parameter.value)
+                }
+            }
+        }.toString()
     }
 }
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Log.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Log.kt
similarity index 98%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Log.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Log.kt
index d2e1d19..a024de2 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Log.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Log.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.camera.camera2.pipe.impl
+package androidx.camera.camera2.pipe.core
 
 import android.util.Log
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Timestamps.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt
similarity index 97%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Timestamps.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt
index 6138029..abb9ba8 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Timestamps.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt
@@ -16,7 +16,7 @@
 
 @file:Suppress("NOTHING_TO_INLINE")
 
-package androidx.camera.camera2.pipe.impl
+package androidx.camera.camera2.pipe.core
 
 import android.os.SystemClock
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CamerasImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraDevicesImpl.kt
similarity index 93%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CamerasImpl.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraDevicesImpl.kt
index 2bd2445..38d80e3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CamerasImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraDevicesImpl.kt
@@ -19,8 +19,8 @@
 import android.hardware.camera2.CameraManager
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.Cameras
-import androidx.camera.camera2.pipe.impl.Debug.trace
+import androidx.camera.camera2.pipe.CameraDevices
+import androidx.camera.camera2.pipe.core.Debug.trace
 import javax.inject.Inject
 import javax.inject.Provider
 import javax.inject.Singleton
@@ -29,10 +29,10 @@
  * Provides utilities for querying cameras and accessing metadata about those cameras.
  */
 @Singleton
-internal class CamerasImpl @Inject constructor(
+internal class CameraDevicesImpl @Inject constructor(
     private val cameraManager: Provider<CameraManager>,
     private val metadata: CameraMetadataCache
-) : Cameras {
+) : CameraDevices {
 
     private val cameras = lazy(LazyThreadSafetyMode.PUBLICATION) {
         // NOTE: Publication safety mode may cause this method to be invoked more than once if there
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt
index ea94610..a024cb3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt
@@ -72,7 +72,7 @@
     @Binds
     abstract fun bindRequestProcessorFactory(
         factory: StandardRequestProcessorFactory
-    ): RequestProcessor.Factory
+    ): RequestProcessorFactory
 
     @Binds
     abstract fun bindGraphState(graphState: GraphStateImpl): GraphState
@@ -107,7 +107,7 @@
             // Listeners in CameraGraph.Config can de defined outside of the CameraPipe library,
             // and since we iterate thought the listeners in order and invoke them, it appears
             // beneficial to add the internal listeners first and then the graph config listeners.
-            listeners.addAll(graphConfig.listeners)
+            listeners.addAll(graphConfig.defaultListeners)
             return listeners
         }
     }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt
index 8f376f5..4d7f06d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt
@@ -19,9 +19,10 @@
 import android.view.Surface
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.Stream
-import androidx.camera.camera2.pipe.StreamConfig
+import androidx.camera.camera2.pipe.StreamGraph
 import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
 import kotlinx.atomicfu.atomic
 import javax.inject.Inject
 
@@ -32,7 +33,7 @@
     graphConfig: CameraGraph.Config,
     metadata: CameraMetadata,
     private val graphProcessor: GraphProcessor,
-    private val streamMap: StreamMap,
+    private val streamGraph: StreamGraphImpl,
     private val graphState: GraphState,
     private val graphState3A: GraphState3A,
     private val listener3A: Listener3A
@@ -46,11 +47,11 @@
 
     init {
         // Log out the configuration of the camera graph when it is created.
-        Debug.logConfiguration(this.toString(), metadata, graphConfig, streamMap)
+        Debug.formatCameraGraphProperties(metadata, graphConfig, this)
     }
 
-    override val streams: Map<StreamConfig, Stream>
-        get() = streamMap.streamConfigMap
+    override val streams: StreamGraph
+        get() = streamGraph
 
     override fun start() {
         Debug.traceStart { "$this#start" }
@@ -84,7 +85,7 @@
 
     override fun setSurface(stream: StreamId, surface: Surface?) {
         Debug.traceStart { "$stream#setSurface" }
-        streamMap[stream] = surface
+        streamGraph[stream] = surface
         Debug.traceStop()
     }
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
index 32710c3..0b632a3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
@@ -67,7 +67,14 @@
         afRegions: List<MeteringRectangle>?,
         awbRegions: List<MeteringRectangle>?
     ): Deferred<Result3A> {
-        return controller3A.update3A(aeMode, afMode, awbMode, aeRegions, afRegions, awbRegions)
+        return controller3A.update3A(
+            aeMode = aeMode,
+            afMode = afMode,
+            awbMode = awbMode,
+            aeRegions = aeRegions,
+            afRegions = afRegions,
+            awbRegions = awbRegions
+        )
     }
 
     override suspend fun submit3A(
@@ -81,8 +88,10 @@
         return controller3A.submit3A(aeMode, afMode, awbMode, aeRegions, afRegions, awbRegions)
     }
 
-    override fun setTorch(torchState: TorchState): Deferred<FrameNumber> {
-        TODO("Implement setTorch")
+    override fun setTorch(torchState: TorchState): Deferred<Result3A> {
+        // TODO(sushilnath): First check whether the camera device has a flash unit. Ref:
+        // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#FLASH_INFO_AVAILABLE
+        return controller3A.setTorch(torchState)
     }
 
     override suspend fun lock3A(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt
index 25d9346..f1b51b7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt
@@ -22,7 +22,10 @@
 import androidx.annotation.GuardedBy
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.Timestamps
+import androidx.camera.camera2.pipe.core.Timestamps.formatMs
 import kotlinx.coroutines.withContext
 import java.lang.IllegalStateException
 import javax.inject.Inject
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt
index aabc119..dfdfe8e 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt
@@ -25,7 +25,11 @@
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.Metadata
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.Timestamps
+import androidx.camera.camera2.pipe.core.Timestamps.formatMs
+import androidx.camera.camera2.pipe.wrapper.Api28Compat
 
 /**
  * This implementation provides access to CameraCharacteristics and lazy caching of properties
@@ -70,7 +74,7 @@
                     characteristics.keys.orEmpty().toSet()
                 }
             } catch (ignored: AssertionError) {
-                emptySet<CameraCharacteristics.Key<*>>()
+                emptySet()
             }
         }
 
@@ -82,7 +86,7 @@
                     characteristics.availableCaptureRequestKeys.orEmpty().toSet()
                 }
             } catch (ignored: AssertionError) {
-                emptySet<CaptureRequest.Key<*>>()
+                emptySet()
             }
         }
 
@@ -94,7 +98,7 @@
                     characteristics.availableCaptureResultKeys.orEmpty().toSet()
                 }
             } catch (ignored: AssertionError) {
-                emptySet<CaptureResult.Key<*>>()
+                emptySet()
             }
         }
 
@@ -110,7 +114,7 @@
                         characteristics.physicalCameraIds.orEmpty().map { CameraId(it) }.toSet()
                     }
                 } catch (ignored: AssertionError) {
-                    emptySet<CameraId>()
+                    emptySet()
                 }
             }
         }
@@ -122,11 +126,12 @@
             } else {
                 try {
                     Debug.trace("Camera-${camera.value}#availablePhysicalCameraRequestKeys") {
-                        @Suppress("UselessCallOnNotNull")
-                        characteristics.availablePhysicalCameraRequestKeys.orEmpty().toSet()
+                        Api28Compat.getAvailablePhysicalCameraRequestKeys(characteristics)
+                            .orEmpty()
+                            .toSet()
                     }
                 } catch (ignored: AssertionError) {
-                    emptySet<CaptureRequest.Key<*>>()
+                    emptySet()
                 }
             }
         }
@@ -138,11 +143,10 @@
             } else {
                 try {
                     Debug.trace("Camera-${camera.value}#availableSessionKeys") {
-                        @Suppress("UselessCallOnNotNull")
-                        characteristics.availableSessionKeys.orEmpty().toSet()
+                        Api28Compat.getAvailableSessionKeys(characteristics).orEmpty().toSet()
                     }
                 } catch (ignored: AssertionError) {
-                    emptySet<CaptureRequest.Key<*>>()
+                    emptySet()
                 }
             }
         }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt
index 587b799..e5f794d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt
@@ -22,7 +22,7 @@
 import android.os.HandlerThread
 import android.os.Process
 import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.Cameras
+import androidx.camera.camera2.pipe.CameraDevices
 import dagger.Binds
 import dagger.Component
 import dagger.Module
@@ -48,7 +48,7 @@
 )
 internal interface CameraPipeComponent {
     fun cameraGraphComponentBuilder(): CameraGraphComponent.Builder
-    fun cameras(): Cameras
+    fun cameras(): CameraDevices
 }
 
 @Module(
@@ -62,7 +62,7 @@
 @Module
 internal abstract class CameraPipeModules {
     @Binds
-    abstract fun bindCameras(impl: CamerasImpl): Cameras
+    abstract fun bindCameras(impl: CameraDevicesImpl): CameraDevices
 
     companion object {
         @Provides
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CaptureSequence.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CaptureSequence.kt
new file mode 100644
index 0000000..8dde9ab
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CaptureSequence.kt
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2020 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.camera.camera2.pipe.impl
+
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CaptureFailure
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.view.Surface
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraTimestamp
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.StreamId
+
+/**
+ * This class responds to events from a set of one or more requests. It uses the tag field on
+ * a CaptureRequest object to lookup and invoke per-request listeners so that a listener can be
+ * defined on a specific request within a burst.
+ */
+internal class CaptureSequence(
+    private val internalListeners: List<Request.Listener>,
+    private val requests: Map<RequestNumber, RequestInfo>,
+    private val captureRequests: List<CaptureRequest>,
+    private val surfaceMap: Map<Surface, StreamId>,
+    private val inFlightRequests: MutableList<CaptureSequence>,
+    private val camera: CameraId
+) : CameraCaptureSession.CaptureCallback() {
+    private val debugId = requestSequenceDebugIds.incrementAndGet()
+
+    @Volatile
+    private var _sequenceNumber: Int? = null
+    var sequenceNumber: Int
+        get() {
+            if (_sequenceNumber == null) {
+                // If the sequence id has not been submitted, it means the call to capture or
+                // setRepeating has not yet returned. The callback methods should never be synchronously
+                // invoked, so the only case this should happen is if a second thread attempted to
+                // invoke one of the callbacks before the initial call completed. By locking against the
+                // captureSequence object here and in the capture call, we can block the callback thread
+                // until the sequenceId is available.
+                synchronized(this) {
+                    return checkNotNull(_sequenceNumber) {
+                        "SequenceNumber has not been set for $this!"
+                    }
+                }
+            }
+            return checkNotNull(_sequenceNumber) {
+                "SequenceNumber has not been set for $this!"
+            }
+        }
+        set(value) {
+            _sequenceNumber = value
+        }
+
+    override fun onCaptureStarted(
+        captureSession: CameraCaptureSession,
+        captureRequest: CaptureRequest,
+        captureTimestamp: Long,
+        captureFrameNumber: Long
+    ) {
+        val requestNumber = readRequestNumber(captureRequest)
+        val timestamp = CameraTimestamp(captureTimestamp)
+        val frameNumber = FrameNumber(captureFrameNumber)
+
+        // Load the request and throw if we are not able to find an associated request. Under
+        // normal circumstances this should never happen.
+        val request = readRequest(requestNumber)
+
+        invokeOnRequest(request) {
+            it.onStarted(
+                request,
+                frameNumber,
+                timestamp
+            )
+        }
+    }
+
+    override fun onCaptureProgressed(
+        captureSession: CameraCaptureSession,
+        captureRequest: CaptureRequest,
+        partialCaptureResult: CaptureResult
+    ) {
+        val requestNumber = readRequestNumber(captureRequest)
+        val frameNumber = FrameNumber(partialCaptureResult.frameNumber)
+        val frameMetadata = AndroidFrameMetadata(partialCaptureResult, camera)
+
+        // Load the request and throw if we are not able to find an associated request. Under
+        // normal circumstances this should never happen.
+        val request = readRequest(requestNumber)
+
+        invokeOnRequest(request) {
+            it.onPartialCaptureResult(
+                request,
+                frameNumber,
+                frameMetadata
+            )
+        }
+    }
+
+    override fun onCaptureCompleted(
+        captureSession: CameraCaptureSession,
+        captureRequest: CaptureRequest,
+        captureResult: TotalCaptureResult
+    ) {
+        // Remove this request from the set of requests that are currently tracked.
+        synchronized(inFlightRequests) {
+            inFlightRequests.remove(this)
+        }
+
+        val requestNumber = readRequestNumber(captureRequest)
+        val frameNumber = FrameNumber(captureResult.frameNumber)
+
+        // Load the request and throw if we are not able to find an associated request. Under
+        // normal circumstances this should never happen.
+        val request = readRequest(requestNumber)
+
+        val frameInfo = AndroidFrameInfo(
+            captureResult,
+            camera,
+            request
+        )
+
+        invokeOnRequest(request) {
+            it.onTotalCaptureResult(
+                request,
+                frameNumber,
+                frameInfo
+            )
+        }
+    }
+
+    override fun onCaptureFailed(
+        captureSession: CameraCaptureSession,
+        captureRequest: CaptureRequest,
+        captureFailure: CaptureFailure
+    ) {
+        // Remove this request from the set of requests that are currently tracked.
+        synchronized(inFlightRequests) {
+            inFlightRequests.remove(this)
+        }
+
+        val requestNumber = readRequestNumber(captureRequest)
+        val frameNumber = FrameNumber(captureFailure.frameNumber)
+
+        // Load the request and throw if we are not able to find an associated request. Under
+        // normal circumstances this should never happen.
+        val request = readRequest(requestNumber)
+
+        invokeOnRequest(request) {
+            it.onFailed(
+                request,
+                frameNumber,
+                captureFailure
+            )
+        }
+    }
+
+    override fun onCaptureBufferLost(
+        captureSession: CameraCaptureSession,
+        captureRequest: CaptureRequest,
+        surface: Surface,
+        frameId: Long
+    ) {
+        val requestNumber = readRequestNumber(captureRequest)
+        val frameNumber = FrameNumber(frameId)
+        val streamId = checkNotNull(surfaceMap[surface]) {
+            "Unable to find the streamId for $surface on frame $frameNumber"
+        }
+
+        // Load the request and throw if we are not able to find an associated request. Under
+        // normal circumstances this should never happen.
+        val request = readRequest(requestNumber)
+
+        invokeOnRequest(request) {
+            it.onBufferLost(
+                request,
+                frameNumber,
+                streamId
+            )
+        }
+    }
+
+    /**
+     * Custom implementation that informs all listeners that the request had not completed when
+     * abort was called.
+     */
+    fun invokeOnAborted() {
+        invokeOnRequests { request, _, listener ->
+            listener.onAborted(request.request)
+        }
+    }
+
+    fun invokeOnRequestSequenceCreated() {
+        invokeOnRequests { request, _, listener ->
+            listener.onRequestSequenceCreated(request)
+        }
+    }
+
+    fun invokeOnRequestSequenceSubmitted() {
+        invokeOnRequests { request, _, listener ->
+            listener.onRequestSequenceSubmitted(request)
+        }
+    }
+
+    override fun onCaptureSequenceCompleted(
+        captureSession: CameraCaptureSession,
+        captureSequenceId: Int,
+        captureFrameNumber: Long
+    ) {
+        check(sequenceNumber == captureSequenceId) {
+            "Complete was invoked on $sequenceNumber, but the sequence was not fully submitted!"
+        }
+        synchronized(inFlightRequests) {
+            inFlightRequests.remove(this)
+        }
+
+        val frameNumber = FrameNumber(captureFrameNumber)
+        invokeOnRequests { request, _, listener ->
+            listener.onRequestSequenceCompleted(request, frameNumber)
+        }
+    }
+
+    override fun onCaptureSequenceAborted(
+        captureSession: CameraCaptureSession,
+        captureSequenceId: Int
+    ) {
+        check(sequenceNumber == captureSequenceId) {
+            "Abort was invoked on $sequenceNumber, but the sequence was not fully submitted!"
+        }
+
+        // Remove this request from the set of requests that are currently tracked.
+        synchronized(inFlightRequests) {
+            inFlightRequests.remove(this)
+        }
+
+        invokeOnRequests { request, _, listener ->
+            listener.onRequestSequenceAborted(request)
+        }
+    }
+
+    private fun readRequestNumber(request: CaptureRequest): RequestNumber =
+        checkNotNull(request.tag as RequestNumber)
+
+    private fun readRequest(requestNumber: RequestNumber): RequestInfo {
+        return checkNotNull(requests[requestNumber]) {
+            "Unable to find the request for $requestNumber!"
+        }
+    }
+
+    private inline fun invokeOnRequests(
+        crossinline fn: (RequestMetadata, Int, Request.Listener) -> Any
+    ) {
+
+        // Always invoke the internal listener first on all of the internal listeners for the
+        // entire sequence before invoking the listeners specified in the specific requests
+        for (i in captureRequests.indices) {
+            val requestNumber = readRequestNumber(captureRequests[i])
+            val request = checkNotNull(requests[requestNumber])
+
+            for (listenerIndex in internalListeners.indices) {
+                fn(request, i, internalListeners[listenerIndex])
+            }
+        }
+
+        for (i in captureRequests.indices) {
+            val requestNumber = readRequestNumber(captureRequests[i])
+            val request = checkNotNull(requests[requestNumber])
+
+            for (listenerIndex in request.request.listeners.indices) {
+                fn(request, i, request.request.listeners[listenerIndex])
+            }
+        }
+    }
+
+    private inline fun invokeOnRequest(
+        request: RequestInfo,
+        crossinline fn: (Request.Listener) -> Any
+    ) {
+        // Always invoke the internal listener first so that internal sate can be updated before
+        // other listeners ask for it.
+        for (i in internalListeners.indices) {
+            fn(internalListeners[i])
+        }
+
+        // Invoke the listeners that were defined on this request.
+        for (i in request.request.listeners.indices) {
+            fn(request.request.listeners[i])
+        }
+    }
+
+    override fun toString(): String = "CaptureSequence-$debugId"
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
index e5d67a5..3133d88 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
@@ -33,10 +33,12 @@
 import androidx.camera.camera2.pipe.AwbMode
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.CameraGraph.Constants3A.FRAME_NUMBER_INVALID
+import androidx.camera.camera2.pipe.FlashMode
 import androidx.camera.camera2.pipe.Lock3ABehavior
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.Status3A
-import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.TorchState
+import androidx.camera.camera2.pipe.core.Log.debug
 import androidx.camera.camera2.pipe.shouldUnlockAe
 import androidx.camera.camera2.pipe.shouldUnlockAf
 import androidx.camera.camera2.pipe.shouldUnlockAwb
@@ -135,26 +137,27 @@
         aeMode: AeMode? = null,
         afMode: AfMode? = null,
         awbMode: AwbMode? = null,
+        flashMode: FlashMode? = null,
         aeRegions: List<MeteringRectangle>? = null,
         afRegions: List<MeteringRectangle>? = null,
         awbRegions: List<MeteringRectangle>? = null
     ): Deferred<Result3A> {
         // Add the listener to a global pool of 3A listeners to monitor the state change to the
         // desired one.
-        val listener = createListenerFor3AParams(aeMode, afMode, awbMode)
+        val listener = createListenerFor3AParams(aeMode, afMode, awbMode, flashMode)
         graphListener3A.addListener(listener)
 
         // Update the 3A state of the graph. This will make sure then when GraphProcessor builds
         // the next request it will apply the 3A parameters corresponding to the updated 3A state
         // to the request.
-        graphState3A.update(aeMode, afMode, awbMode, aeRegions, afRegions, awbRegions, null, null)
+        graphState3A.update(aeMode, afMode, awbMode, flashMode, aeRegions, afRegions, awbRegions)
         // Try submitting a new repeating request with the 3A parameters corresponding to the new
         // 3A state and corresponding listeners.
         graphProcessor.invalidate()
 
         val result = listener.getDeferredResult()
         synchronized(this) {
-            lastUpdate3AResult?.cancel("A newer update3A call initiated.")
+            lastUpdate3AResult?.cancel("A newer call for 3A state update initiated.")
             lastUpdate3AResult = result
         }
         return result
@@ -228,7 +231,7 @@
         afLockBehavior: Lock3ABehavior? = null,
         awbLockBehavior: Lock3ABehavior? = null,
         frameLimit: Int = CameraGraph.DEFAULT_FRAME_LIMIT,
-        timeLimitMsNs: Long? = CameraGraph.DEFAULT_TIME_LIMIT_NS
+        timeLimitNs: Long? = CameraGraph.DEFAULT_TIME_LIMIT_NS
     ): Deferred<Result3A> {
         // If we explicitly need to unlock af first before proceeding to lock it, we need to send
         // a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
@@ -252,7 +255,7 @@
             val listener = Result3AStateListenerImpl(
                 converged3AExitConditions,
                 frameLimit,
-                timeLimitMsNs
+                timeLimitNs
             )
             graphListener3A.addListener(listener)
 
@@ -291,7 +294,7 @@
             }
         }
 
-        return lock3ANow(aeLockBehavior, afLockBehavior, awbLockBehavior, frameLimit, timeLimitMsNs)
+        return lock3ANow(aeLockBehavior, afLockBehavior, awbLockBehavior, frameLimit, timeLimitNs)
     }
 
     /**
@@ -446,12 +449,22 @@
         return listener.getDeferredResult()
     }
 
+    fun setTorch(torchState: TorchState): Deferred<Result3A> {
+        // Determine the flash mode based on the torch state.
+        val flashMode = if (torchState == TorchState.ON) FlashMode.TORCH else FlashMode.OFF
+        // To use the flash control, AE mode must be set to ON or OFF.
+        val currAeMode = graphState3A.aeMode
+        val desiredAeMode = if (currAeMode == AeMode.ON || currAeMode == AeMode.OFF) null else
+            AeMode.ON
+        return update3A(aeMode = desiredAeMode, flashMode = flashMode)
+    }
+
     private suspend fun lock3ANow(
         aeLockBehavior: Lock3ABehavior?,
         afLockBehavior: Lock3ABehavior?,
         awbLockBehavior: Lock3ABehavior?,
         frameLimit: Int?,
-        timeLimitMsNs: Long?
+        timeLimitNs: Long?
     ): Deferred<Result3A> {
         val finalAeLockValue = if (aeLockBehavior == null) null else true
         val finalAwbLockValue = if (awbLockBehavior == null) null else true
@@ -466,7 +479,7 @@
             val listener = Result3AStateListenerImpl(
                 locked3AExitConditions,
                 frameLimit,
-                timeLimitMsNs
+                timeLimitNs
             )
             graphListener3A.addListener(listener)
             graphState3A.update(aeLock = finalAeLockValue, awbLock = finalAwbLockValue)
@@ -566,14 +579,16 @@
     // exact match between the metering regions sent in the capture request and the metering
     // regions received from the camera device.
     private fun createListenerFor3AParams(
-        aeMode: AeMode?,
-        afMode: AfMode?,
-        awbMode: AwbMode?
+        aeMode: AeMode? = null,
+        afMode: AfMode? = null,
+        awbMode: AwbMode? = null,
+        flashMode: FlashMode? = null,
     ): Result3AStateListenerImpl {
         val resultModesMap = mutableMapOf<CaptureResult.Key<*>, List<Any>>()
         aeMode?.let { resultModesMap.put(CaptureResult.CONTROL_AE_MODE, listOf(it.value)) }
         afMode?.let { resultModesMap.put(CaptureResult.CONTROL_AF_MODE, listOf(it.value)) }
         awbMode?.let { resultModesMap.put(CaptureResult.CONTROL_AWB_MODE, listOf(it.value)) }
+        flashMode?.let { resultModesMap.put(CaptureResult.FLASH_MODE, listOf(it.value)) }
         return Result3AStateListenerImpl(resultModesMap.toMap())
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/FrameMetadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/FrameMetadata.kt
index 65e1ef0..c5cf660 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/FrameMetadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/FrameMetadata.kt
@@ -26,6 +26,7 @@
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.Metadata
 import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.wrapper.Api28Compat
 
 /**
  * An implementation of [FrameMetadata] that retrieves values from a [CaptureResult] object
@@ -102,8 +103,8 @@
         // Metadata for physical cameras was introduced in Android P so that it can be used to
         // determine state of the physical lens and sensor in a multi-camera configuration.
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
-            val physicalResults = totalCaptureResult.physicalCameraResults
-            if (physicalResults.isNotEmpty()) {
+            val physicalResults = Api28Compat.getPhysicalCaptureResults(totalCaptureResult)
+            if (physicalResults != null && physicalResults.isNotEmpty()) {
                 val map = ArrayMap<CameraId, AndroidFrameMetadata>(physicalResults.size)
                 for (entry in physicalResults) {
                     val physicalCamera = CameraId(entry.key)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessor.kt
index 801c88a..856a94e 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessor.kt
@@ -16,12 +16,13 @@
 
 package androidx.camera.camera2.pipe.impl
 
-import android.hardware.camera2.CaptureRequest
 import androidx.annotation.GuardedBy
+import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log.debug
+import androidx.camera.camera2.pipe.core.Log.warn
 import androidx.camera.camera2.pipe.formatForLogs
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Log.warn
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -36,7 +37,7 @@
     fun setRepeating(request: Request)
     fun submit(request: Request)
     fun submit(requests: List<Request>)
-    suspend fun submit(parameters: Map<CaptureRequest.Key<*>, Any>): Boolean
+    suspend fun submit(parameters: Map<*, Any>): Boolean
 
     /**
      * Abort all submitted requests that have not yet been submitted to the [RequestProcessor] as
@@ -61,6 +62,7 @@
 @CameraGraphScope
 internal class GraphProcessorImpl @Inject constructor(
     private val threads: Threads,
+    private val cameraGraphConfig: CameraGraph.Config,
     @ForCameraGraph private val graphScope: CoroutineScope,
     @ForCameraGraph private val graphListeners: java.util.ArrayList<Request.Listener>
 ) : GraphProcessor {
@@ -176,7 +178,7 @@
     /**
      * Submit a request to the camera using only the current repeating request.
      */
-    override suspend fun submit(parameters: Map<CaptureRequest.Key<*>, Any>): Boolean =
+    override suspend fun submit(parameters: Map<*, Any>): Boolean =
         withContext(threads.ioDispatcher) {
             val processor: RequestProcessor?
             val request: Request?
@@ -191,8 +193,8 @@
                 processor == null || request == null -> false
                 else -> processor.submit(
                     request,
-                    parameters,
-                    requireSurfacesForAllStreams = false
+                    defaultParameters = cameraGraphConfig.defaultParameters,
+                    requiredParameters = parameters
                 )
             }
         }
@@ -246,11 +248,6 @@
         }
     }
 
-    private fun read3AState(): Map<CaptureRequest.Key<*>, Any> {
-        // TODO: Build extras from 3A state
-        return mapOf()
-    }
-
     private fun abortBurst(requests: List<Request>) {
         for (request in requests) {
             abortRequest(request)
@@ -281,10 +278,13 @@
         if (processor != null && request != null) {
 
             Debug.traceStart { "$this#setRepeating" }
-            val extras: Map<CaptureRequest.Key<*>, Any> = read3AState()
-
             synchronized(processor) {
-                if (processor.setRepeating(request, extras, requireSurfacesForAllStreams = true)) {
+                if (processor.setRepeating(
+                        request,
+                        cameraGraphConfig.defaultParameters,
+                        emptyMap<Any, Any>()
+                    )
+                ) {
                     // ONLY update the current repeating request if the update succeeds
                     synchronized(lock) {
                         if (processor === _requestProcessor) {
@@ -332,12 +332,19 @@
             var submitted = false
             Debug.traceStart { "$this#submit" }
             try {
-                val extras: Map<CaptureRequest.Key<*>, Any> = read3AState()
                 submitted = synchronized(processor) {
                     if (burst.size == 1) {
-                        processor.submit(burst[0], extras, true)
+                        processor.submit(
+                            burst[0],
+                            cameraGraphConfig.defaultParameters,
+                            emptyMap<Any, Any>()
+                        )
                     } else {
-                        processor.submit(burst, extras, true)
+                        processor.submit(
+                            burst,
+                            cameraGraphConfig.defaultParameters,
+                            emptyMap<Any, Any>()
+                        )
                     }
                 }
             } finally {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState.kt
index 9504ebe..c59b50f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState.kt
@@ -43,9 +43,9 @@
     private val config: CameraGraph.Config,
     private val graphProcessor: GraphProcessor,
     private val sessionFactory: SessionFactory,
-    private val requestProcessorFactory: RequestProcessor.Factory,
+    private val requestProcessorFactory: RequestProcessorFactory,
     private val virtualCameraManager: VirtualCameraManager,
-    private val streamMap: StreamMap
+    private val streamGraph: StreamGraphImpl
 ) : GraphState {
     private var currentCamera: VirtualCamera? = null
     private var currentSession: VirtualSessionState? = null
@@ -120,7 +120,7 @@
         }
 
         if (camera != null && session != null) {
-            streamMap.listener = session
+            streamGraph.listener = session
             camera.state.collect {
                 if (it is CameraStateOpen) {
                     session.cameraDevice = it.cameraDevice
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt
index a660f54..6e8a0f2 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt
@@ -21,6 +21,7 @@
 import androidx.camera.camera2.pipe.AeMode
 import androidx.camera.camera2.pipe.AfMode
 import androidx.camera.camera2.pipe.AwbMode
+import androidx.camera.camera2.pipe.FlashMode
 import javax.inject.Inject
 
 /**
@@ -37,19 +38,39 @@
  */
 @CameraGraphScope
 internal class GraphState3A @Inject constructor() {
-    private var aeMode: AeMode? = null
-    private var afMode: AfMode? = null
-    private var awbMode: AwbMode? = null
-    private var aeRegions: List<MeteringRectangle>? = null
-    private var afRegions: List<MeteringRectangle>? = null
-    private var awbRegions: List<MeteringRectangle>? = null
-    private var aeLock: Boolean? = null
-    private var awbLock: Boolean? = null
+    var aeMode: AeMode? = null
+        get() = synchronized(this) { field }
+        private set
+    var afMode: AfMode? = null
+        get() = synchronized(this) { field }
+        private set
+    var awbMode: AwbMode? = null
+        get() = synchronized(this) { field }
+        private set
+    var flashMode: FlashMode? = null
+        get() = synchronized(this) { field }
+        private set
+    var aeRegions: List<MeteringRectangle>? = null
+        get() = synchronized(this) { field }
+        private set
+    var afRegions: List<MeteringRectangle>? = null
+        get() = synchronized(this) { field }
+        private set
+    var awbRegions: List<MeteringRectangle>? = null
+        get() = synchronized(this) { field }
+        private set
+    var aeLock: Boolean? = null
+        get() = synchronized(this) { field }
+        private set
+    var awbLock: Boolean? = null
+        get() = synchronized(this) { field }
+        private set
 
     fun update(
         aeMode: AeMode? = null,
         afMode: AfMode? = null,
         awbMode: AwbMode? = null,
+        flashMode: FlashMode? = null,
         aeRegions: List<MeteringRectangle>? = null,
         afRegions: List<MeteringRectangle>? = null,
         awbRegions: List<MeteringRectangle>? = null,
@@ -60,6 +81,7 @@
             aeMode?.let { this.aeMode = it }
             afMode?.let { this.afMode = it }
             awbMode?.let { this.awbMode = it }
+            flashMode?.let { this.flashMode = it }
             aeRegions?.let { this.aeRegions = it }
             afRegions?.let { this.afRegions = it }
             awbRegions?.let { this.awbRegions = it }
@@ -74,6 +96,7 @@
             aeMode?.let { map.put(CaptureRequest.CONTROL_AE_MODE, it.value) }
             afMode?.let { map.put(CaptureRequest.CONTROL_AF_MODE, it.value) }
             awbMode?.let { map.put(CaptureRequest.CONTROL_AWB_MODE, it.value) }
+            flashMode?.let { map.put(CaptureRequest.FLASH_MODE, it.value) }
             aeRegions?.let { map.put(CaptureRequest.CONTROL_AE_REGIONS, it.toTypedArray()) }
             afRegions?.let { map.put(CaptureRequest.CONTROL_AF_REGIONS, it.toTypedArray()) }
             awbRegions?.let { map.put(CaptureRequest.CONTROL_AWB_REGIONS, it.toTypedArray()) }
@@ -88,6 +111,7 @@
             aeMode?.let { builder.set(CaptureRequest.CONTROL_AE_MODE, it.value) }
             afMode?.let { builder.set(CaptureRequest.CONTROL_AF_MODE, it.value) }
             awbMode?.let { builder.set(CaptureRequest.CONTROL_AWB_MODE, it.value) }
+            flashMode?.let { builder.set(CaptureRequest.FLASH_MODE, it.value) }
             aeRegions?.let { builder.set(CaptureRequest.CONTROL_AE_REGIONS, it.toTypedArray()) }
             afRegions?.let { builder.set(CaptureRequest.CONTROL_AF_REGIONS, it.toTypedArray()) }
             awbRegions?.let { builder.set(CaptureRequest.CONTROL_AWB_REGIONS, it.toTypedArray()) }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Permissions.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Permissions.kt
index cabfe98..9271e28ec 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Permissions.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Permissions.kt
@@ -22,6 +22,7 @@
 import android.content.pm.PackageManager.PERMISSION_GRANTED
 import android.os.Build
 import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.core.Debug
 import javax.inject.Inject
 import javax.inject.Singleton
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
index 6668b71..69b9ac0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
@@ -16,32 +16,8 @@
 
 package androidx.camera.camera2.pipe.impl
 
-import android.hardware.camera2.CameraAccessException
-import android.hardware.camera2.CameraCaptureSession
-import android.hardware.camera2.CaptureFailure
 import android.hardware.camera2.CaptureRequest
-import android.hardware.camera2.CaptureResult
-import android.hardware.camera2.TotalCaptureResult
-import android.util.ArrayMap
-import android.view.Surface
-import androidx.annotation.GuardedBy
-import androidx.camera.camera2.pipe.CameraGraph
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraTimestamp
-import androidx.camera.camera2.pipe.FrameNumber
-import androidx.camera.camera2.pipe.Metadata
 import androidx.camera.camera2.pipe.Request
-import androidx.camera.camera2.pipe.RequestMetadata
-import androidx.camera.camera2.pipe.RequestNumber
-import androidx.camera.camera2.pipe.RequestTemplate
-import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.wrapper.CameraCaptureSessionWrapper
-import androidx.camera.camera2.pipe.wrapper.ObjectUnavailableException
-import androidx.camera.camera2.pipe.writeParameters
-import kotlinx.atomicfu.atomic
-import java.util.Collections.singletonList
-import java.util.Collections.singletonMap
-import javax.inject.Inject
 
 /**
  * An instance of a RequestProcessor exists for the duration of a CameraCaptureSession and must be
@@ -55,47 +31,39 @@
  *   global and *are* thread safe.
  * - Special care is taken to reduce the number objects and wrappers that are created, and to reduce
  *   the number of loops and overhead in wrapper objects.
- * - Callbacks are expected to be invoked at very high frequency.
+ * - Callbacks are expected to be invoked at *very* high frequency on the camera thread.
  * - One RequestProcessor instance per CameraCaptureSession
  */
-internal interface RequestProcessor {
+interface RequestProcessor {
 
     /**
      * Submit a single [Request] with an optional set of extra parameters.
      *
      * @param request the request to submit to the camera.
-     * @param extraRequestParameters extra parameters to apply to the request.
-     * @param requireSurfacesForAllStreams if this flag is defined then this method will only submit
-     *   the request if all streamIds can be mapped to valid surfaces. At least one surface is
-     *   always required. This is useful if (for example) someone needs to quickly submit a
-     *   request with a specific trigger or mode key but does not care about modifying the list of
-     *   current surfaces.
+     * @param defaultParameters will not override parameters specified in the request.
+     * @param requiredParameters will override parameters specified in the request.
      * @return false if this request failed to be submitted. If this method returns false, none of
      *   the callbacks on the Request(s) will be invoked.
      */
     fun submit(
         request: Request,
-        extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
-        requireSurfacesForAllStreams: Boolean
+        defaultParameters: Map<*, Any>,
+        requiredParameters: Map<*, Any>
     ): Boolean
 
     /**
      * Submit a list of [Request]s with an optional set of extra parameters.
      *
      * @param requests the requests to submit to the camera.
-     * @param extraRequestParameters extra parameters to apply to the request.
-     * @param requireSurfacesForAllStreams if this flag is defined then this method will only submit
-     *   the request if all streamIds can be mapped to valid surfaces. At least one surface is
-     *   always required. This is useful if (for example) someone needs to quickly submit a
-     *   request with a specific trigger or mode key but does not care about modifying the list of
-     *   current surfaces.
+     * @param defaultParameters will not override parameters specified in the request.
+     * @param requiredParameters will override parameters specified in the request.
      * @return false if this request failed to be submitted. If this method returns false, none of
      *   the callbacks on the Request(s) will be invoked.
      */
     fun submit(
         requests: List<Request>,
-        extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
-        requireSurfacesForAllStreams: Boolean
+        defaultParameters: Map<*, Any>,
+        requiredParameters: Map<*, Any>
     ): Boolean
 
     /**
@@ -106,19 +74,15 @@
      * cause the request to be used to generate multiple [CaptureRequest]s to the camera.
      *
      * @param request the requests to set as the repeating request.
-     * @param extraRequestParameters extra parameters to apply to the request.
-     * @param requireSurfacesForAllStreams if this flag is defined then this method will only submit
-     *   the request if all streamIds can be mapped to valid surfaces. At least one surface is
-     *   always required. This is useful if (for example) someone needs to quickly submit a
-     *   request with a specific trigger or mode key but does not care about modifying the list of
-     *   current surfaces.
+     * @param defaultParameters will not override parameters specified in the request.
+     * @param requiredParameters will override parameters specified in the request.
      * @return false if this request failed to be submitted. If this method returns false, none of
      *   the callbacks on the Request(s) will be invoked.
      */
     fun setRepeating(
         request: Request,
-        extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
-        requireSurfacesForAllStreams: Boolean
+        defaultParameters: Map<*, Any>,
+        requiredParameters: Map<*, Any>
     ): Boolean
 
     /**
@@ -133,627 +97,7 @@
 
     /**
      * Puts the RequestProcessor into a closed state where it will reject all incoming requests.
+     * This does NOT call stopRepeating() or abortCaptures().
      */
     fun close()
-
-    interface Factory {
-        fun create(
-            session: CameraCaptureSessionWrapper,
-            surfaceMap: Map<StreamId, Surface>
-        ): RequestProcessor
-    }
-}
-
-internal class StandardRequestProcessorFactory @Inject constructor(
-    private val threads: Threads,
-    private val graphConfig: CameraGraph.Config,
-    @ForCameraGraph private val graphListeners: ArrayList<Request.Listener>,
-    private val graphState3A: GraphState3A
-) : RequestProcessor.Factory {
-    override fun create(
-        session: CameraCaptureSessionWrapper,
-        surfaceMap: Map<StreamId, Surface>
-    ): RequestProcessor =
-        StandardRequestProcessor(
-            session,
-            threads,
-            graphConfig,
-            surfaceMap,
-            graphListeners,
-            graphState3A
-        )
-}
-
-internal val requestProcessorDebugIds = atomic(0)
-internal val requestSequenceDebugIds = atomic(0L)
-internal val requestTags = atomic(0L)
-internal fun nextRequestTag(): RequestNumber = RequestNumber(requestTags.incrementAndGet())
-
-/**
- * This class is designed to synchronously handle interactions with the Camera CaptureSession.
- */
-internal class StandardRequestProcessor(
-    private val session: CameraCaptureSessionWrapper,
-    private val threads: Threads,
-    private val graphConfig: CameraGraph.Config,
-    private val surfaceMap: Map<StreamId, Surface>,
-    private val graphListeners: List<Request.Listener>,
-    private val graphState3A: GraphState3A
-) : RequestProcessor {
-
-    @GuardedBy("inFlightRequests")
-    private val inFlightRequests = mutableListOf<CaptureSequence>()
-    private val debugId = requestProcessorDebugIds.incrementAndGet()
-    private val closed = atomic(false)
-
-    override fun submit(
-        request: Request,
-        extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
-        requireSurfacesForAllStreams: Boolean
-    ): Boolean {
-        return configureAndCapture(
-            singletonList(request),
-            extraRequestParameters,
-            requireSurfacesForAllStreams,
-            isRepeating = false
-        )
-    }
-
-    override fun submit(
-        requests: List<Request>,
-        extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
-        requireSurfacesForAllStreams: Boolean
-    ): Boolean {
-        return configureAndCapture(
-            requests,
-            extraRequestParameters,
-            requireSurfacesForAllStreams,
-            isRepeating = false
-        )
-    }
-
-    override fun setRepeating(
-        request: Request,
-        extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
-        requireSurfacesForAllStreams: Boolean
-    ): Boolean {
-        return configureAndCapture(
-            singletonList(request),
-            extraRequestParameters,
-            requireSurfacesForAllStreams,
-            isRepeating = true
-        )
-    }
-
-    override fun abortCaptures() {
-        val requestsToAbort = synchronized(inFlightRequests) {
-            val copy = inFlightRequests.toList()
-            inFlightRequests.clear()
-            copy
-        }
-        for (sequence in requestsToAbort) {
-            sequence.invokeOnAborted()
-        }
-        session.abortCaptures()
-    }
-
-    override fun stopRepeating() {
-        session.stopRepeating()
-    }
-
-    override fun close() {
-        closed.compareAndSet(expect = false, update = true)
-    }
-
-    private fun configureAndCapture(
-        requests: List<Request>,
-        extras: Map<CaptureRequest.Key<*>, Any>,
-        requireStreams: Boolean,
-        isRepeating: Boolean
-    ): Boolean {
-        // Reject incoming requests if this instance has been stopped or closed.
-        if (closed.value) {
-            return false
-        }
-
-        val requestMap = ArrayMap<RequestNumber, RequestInfo>(requests.size)
-        val captureRequests = ArrayList<CaptureRequest>(requests.size)
-
-        val surfaceToStreamMap = ArrayMap<Surface, StreamId>()
-        val streamToSurfaceMap = ArrayMap<StreamId, Surface>()
-
-        for (request in requests) {
-            val requestTemplate = request.template ?: graphConfig.template
-
-            Log.debug { "Building CaptureRequest for $request" }
-
-            // Check to see if there is at least one valid surface for each stream.
-            var hasSurface = false
-            for (stream in request.streams) {
-                if (streamToSurfaceMap.contains(stream)) {
-                    hasSurface = true
-                    continue
-                }
-
-                val surface = surfaceMap[stream]
-                if (surface != null) {
-                    Log.debug { "  Binding $stream to $surface" }
-
-                    // TODO(codelogic) There should be a more efficient way to do these lookups than
-                    // having two maps.
-                    surfaceToStreamMap[surface] = stream
-                    streamToSurfaceMap[stream] = surface
-                    hasSurface = true
-                } else if (requireStreams) {
-                    Log.info { "  Failed to bind surface to $stream" }
-                    // If requireStreams is set we are required to map every stream to a valid
-                    // Surface object for this request. If this condition is violated, then we
-                    // return false because we cannot submit these request(s) until there is a valid
-                    // StreamId -> Surface mapping for all streams.
-                    return false
-                }
-            }
-
-            // If there are no surfaces on a particular request, camera2 will now allow us to
-            // submit it.
-            if (!hasSurface) {
-                return false
-            }
-
-            // Create the request builder. There is a risk this will throw an exception or return null
-            // if the CameraDevice has been closed or disconnected. If this fails, indicate that the
-            // request was not submitted.
-            val requestBuilder: CaptureRequest.Builder
-            try {
-                requestBuilder = session.device.createCaptureRequest(requestTemplate)
-            } catch (exception: ObjectUnavailableException) {
-                return false
-            }
-
-            // Apply the output surfaces to the requestBuilder
-            hasSurface = false
-            for (stream in request.streams) {
-                val surface = streamToSurfaceMap[stream]
-                if (surface != null) {
-                    requestBuilder.addTarget(surface)
-                    hasSurface = true
-                }
-            }
-
-            // Soundness check to make sure we add at least one surface. This should be guaranteed
-            // because we are supposed to exit early and return false if we cannot map at least one
-            // surface per request.
-            check(hasSurface)
-
-            // Apply the parameters to the requestBuilder
-            requestBuilder.writeParameters(request.requestParameters)
-
-            // Apply the 3A parameters first. This gives the users of camerapipe the ability to
-            // still override the 3A parameters for complicated use cases.
-            //
-            // TODO(sushilnath@): Implement one of the two options. (1) Apply the 3A parameters
-            // from internal 3A state machine at last and provide a flag in the Request object to
-            // specify when the clients want to explicitly override some of the 3A parameters
-            // directly. Add code to handle the flag. (2) Let clients override the 3A parameters
-            // freely and when that happens intercept those parameters from the request and keep the
-            // internal 3A state machine in sync.
-            graphState3A.writeTo(requestBuilder)
-
-            // Write extra parameters to the request. These parameters will overwite parameters
-            // defined in the Request (if they overlap)
-            requestBuilder.writeParameters(extras)
-
-            // The tag must be set for every request. We use it to lookup listeners for the
-            // individual requests so that each request can specify individual listeners.
-            val requestTag = nextRequestTag()
-            requestBuilder.setTag(requestTag)
-
-            // Create the camera2 captureRequest and add it to our list of requests.
-            val captureRequest = requestBuilder.build()
-            captureRequests.add(captureRequest)
-
-            @Suppress("SyntheticAccessor")
-            requestMap[requestTag] = RequestInfo(
-                captureRequest,
-                emptyMap(),
-                streamToSurfaceMap,
-                requestTemplate,
-                isRepeating,
-                request,
-                requestTag
-            )
-        }
-
-        // Create the captureSequence listener
-        @Suppress("SyntheticAccessor")
-        val captureSequence = CaptureSequence(
-            graphListeners,
-            if (requests.size == 1) {
-                singletonMap(requestMap.keyAt(0), requestMap.valueAt(0))
-            } else {
-                requestMap
-            },
-            captureRequests,
-            surfaceToStreamMap,
-            inFlightRequests,
-            session.device.cameraId
-        )
-
-        // Non-repeating requests must always be aware of abort calls.
-        if (!isRepeating) {
-            synchronized(inFlightRequests) {
-                inFlightRequests.add(captureSequence)
-            }
-        }
-
-        var captured = false
-        return try {
-
-            Log.debug { "Submitting $captureSequence" }
-            capture(captureRequests, captureSequence, isRepeating)
-            captured = true
-            Log.debug { "Submitted $captureSequence" }
-            true
-        } catch (closedException: ObjectUnavailableException) {
-            false
-        } catch (accessException: CameraAccessException) {
-            false
-        } finally {
-            // If ANY unhandled exception occurs, don't throw, but make sure we remove it from the
-            // list of in-flight requests.
-            if (!captured && !isRepeating) {
-                captureSequence.invokeOnAborted()
-            }
-        }
-    }
-
-    private fun capture(
-        captureRequests: List<CaptureRequest>,
-        captureSequence: CaptureSequence,
-        isRepeating: Boolean
-    ) {
-        captureSequence.invokeOnRequestSequenceCreated()
-
-        // NOTE: This is a funny synchronization call. The purpose is to avoid a rare but possible
-        // situation where calling capture causes one of the callback methods to be invoked before
-        // sequenceNumber has been set on the callback. Both this call and the synchronized
-        // behavior on the CaptureSequence listener have been designed to minimize the number of
-        // synchronized calls.
-        synchronized(lock = captureSequence) {
-            // TODO: Update these calls to use executors on newer versions of the OS
-            val sequenceNumber: Int = if (captureRequests.size == 1) {
-                if (isRepeating) {
-                    session.setRepeatingRequest(
-                        captureRequests[0],
-                        captureSequence,
-                        threads.camera2Handler
-                    )
-                } else {
-                    session.capture(captureRequests[0], captureSequence, threads.camera2Handler)
-                }
-            } else {
-                if (isRepeating) {
-                    session.setRepeatingBurst(
-                        captureRequests,
-                        captureSequence,
-                        threads.camera2Handler
-                    )
-                } else {
-                    session.captureBurst(captureRequests, captureSequence, threads.camera2Handler)
-                }
-            }
-            captureSequence.sequenceNumber = sequenceNumber
-        }
-
-        // Invoke callbacks without holding a lock.
-        captureSequence.invokeOnRequestSequenceSubmitted()
-    }
-
-    override fun toString(): String {
-        return "RequestProcessor-$debugId"
-    }
-}
-
-/**
- * This class packages together information about a request that was submitted to the camera.
- */
-@Suppress("SyntheticAccessor") // Using an inline class generates a synthetic constructor
-internal class RequestInfo(
-    private val captureRequest: CaptureRequest,
-    private val extraRequestParameters: Map<Metadata.Key<*>, Any?>,
-    override val streams: Map<StreamId, Surface>,
-    override val template: RequestTemplate,
-    override val repeating: Boolean,
-    override val request: Request,
-    override val requestNumber: RequestNumber
-) : RequestMetadata {
-    override fun <T> get(key: CaptureRequest.Key<T>): T? = captureRequest[key]
-    override fun <T> getOrDefault(key: CaptureRequest.Key<T>, default: T): T =
-        get(key) ?: default
-
-    @Suppress("UNCHECKED_CAST")
-    override fun <T> get(key: Metadata.Key<T>): T? = extraRequestParameters[key] as T?
-
-    override fun <T> getOrDefault(key: Metadata.Key<T>, default: T): T = get(key) ?: default
-
-    override fun unwrap(): CaptureRequest = captureRequest
-}
-
-/**
- * This class responds to events from a set of one or more requests. It uses the tag field on
- * a CaptureRequest object to lookup and invoke per-request listeners so that a listener can be
- * defined on a specific request within a burst.
- */
-internal class CaptureSequence(
-    private val internalListeners: List<Request.Listener>,
-    private val requests: Map<RequestNumber, RequestInfo>,
-    private val captureRequests: List<CaptureRequest>,
-    private val surfaceMap: Map<Surface, StreamId>,
-    private val inFlightRequests: MutableList<CaptureSequence>,
-    private val camera: CameraId
-) : CameraCaptureSession.CaptureCallback() {
-    private val debugId = requestSequenceDebugIds.incrementAndGet()
-
-    @Volatile
-    private var _sequenceNumber: Int? = null
-    var sequenceNumber: Int
-        get() {
-            if (_sequenceNumber == null) {
-                // If the sequence id has not been submitted, it means the call to capture or
-                // setRepeating has not yet returned. The callback methods should never be synchronously
-                // invoked, so the only case this should happen is if a second thread attempted to
-                // invoke one of the callbacks before the initial call completed. By locking against the
-                // captureSequence object here and in the capture call, we can block the callback thread
-                // until the sequenceId is available.
-                synchronized(this) {
-                    return checkNotNull(_sequenceNumber) {
-                        "SequenceNumber has not been set for $this!"
-                    }
-                }
-            }
-            return checkNotNull(_sequenceNumber) {
-                "SequenceNumber has not been set for $this!"
-            }
-        }
-        set(value) {
-            _sequenceNumber = value
-        }
-
-    override fun onCaptureStarted(
-        captureSession: CameraCaptureSession,
-        captureRequest: CaptureRequest,
-        captureTimestamp: Long,
-        captureFrameNumber: Long
-    ) {
-        val requestNumber = readRequestNumber(captureRequest)
-        val timestamp = CameraTimestamp(captureTimestamp)
-        val frameNumber = FrameNumber(captureFrameNumber)
-
-        // Load the request and throw if we are not able to find an associated request. Under
-        // normal circumstances this should never happen.
-        val request = readRequest(requestNumber)
-
-        invokeOnRequest(request) {
-            it.onStarted(
-                request,
-                frameNumber,
-                timestamp
-            )
-        }
-    }
-
-    override fun onCaptureProgressed(
-        captureSession: CameraCaptureSession,
-        captureRequest: CaptureRequest,
-        partialCaptureResult: CaptureResult
-    ) {
-        val requestNumber = readRequestNumber(captureRequest)
-        val frameNumber = FrameNumber(partialCaptureResult.frameNumber)
-        val frameMetadata = AndroidFrameMetadata(partialCaptureResult, camera)
-
-        // Load the request and throw if we are not able to find an associated request. Under
-        // normal circumstances this should never happen.
-        val request = readRequest(requestNumber)
-
-        invokeOnRequest(request) {
-            it.onPartialCaptureResult(
-                request,
-                frameNumber,
-                frameMetadata
-            )
-        }
-    }
-
-    override fun onCaptureCompleted(
-        captureSession: CameraCaptureSession,
-        captureRequest: CaptureRequest,
-        captureResult: TotalCaptureResult
-    ) {
-        // Remove this request from the set of requests that are currently tracked.
-        synchronized(inFlightRequests) {
-            inFlightRequests.remove(this)
-        }
-
-        val requestNumber = readRequestNumber(captureRequest)
-        val frameNumber = FrameNumber(captureResult.frameNumber)
-
-        // Load the request and throw if we are not able to find an associated request. Under
-        // normal circumstances this should never happen.
-        val request = readRequest(requestNumber)
-
-        val frameInfo = AndroidFrameInfo(
-            captureResult,
-            camera,
-            request
-        )
-
-        invokeOnRequest(request) {
-            it.onTotalCaptureResult(
-                request,
-                frameNumber,
-                frameInfo
-            )
-        }
-    }
-
-    override fun onCaptureFailed(
-        captureSession: CameraCaptureSession,
-        captureRequest: CaptureRequest,
-        captureFailure: CaptureFailure
-    ) {
-        // Remove this request from the set of requests that are currently tracked.
-        synchronized(inFlightRequests) {
-            inFlightRequests.remove(this)
-        }
-
-        val requestNumber = readRequestNumber(captureRequest)
-        val frameNumber = FrameNumber(captureFailure.frameNumber)
-
-        // Load the request and throw if we are not able to find an associated request. Under
-        // normal circumstances this should never happen.
-        val request = readRequest(requestNumber)
-
-        invokeOnRequest(request) {
-            it.onFailed(
-                request,
-                frameNumber,
-                captureFailure
-            )
-        }
-    }
-
-    override fun onCaptureBufferLost(
-        captureSession: CameraCaptureSession,
-        captureRequest: CaptureRequest,
-        surface: Surface,
-        frameId: Long
-    ) {
-        val requestNumber = readRequestNumber(captureRequest)
-        val frameNumber = FrameNumber(frameId)
-        val streamId = checkNotNull(surfaceMap[surface]) {
-            "Unable to find the streamId for $surface on frame $frameNumber"
-        }
-
-        // Load the request and throw if we are not able to find an associated request. Under
-        // normal circumstances this should never happen.
-        val request = readRequest(requestNumber)
-
-        invokeOnRequest(request) {
-            it.onBufferLost(
-                request,
-                frameNumber,
-                streamId
-            )
-        }
-    }
-
-    /**
-     * Custom implementation that informs all listeners that the request had not completed when
-     * abort was called.
-     */
-    fun invokeOnAborted() {
-        invokeOnRequests { request, _, listener ->
-            listener.onAborted(request.request)
-        }
-    }
-
-    fun invokeOnRequestSequenceCreated() {
-        invokeOnRequests { request, _, listener ->
-            listener.onRequestSequenceCreated(request)
-        }
-    }
-
-    fun invokeOnRequestSequenceSubmitted() {
-        invokeOnRequests { request, _, listener ->
-            listener.onRequestSequenceSubmitted(request)
-        }
-    }
-
-    override fun onCaptureSequenceCompleted(
-        captureSession: CameraCaptureSession,
-        captureSequenceId: Int,
-        captureFrameNumber: Long
-    ) {
-        check(sequenceNumber == captureSequenceId) {
-            "Complete was invoked on $sequenceNumber, but the sequence was not fully submitted!"
-        }
-        synchronized(inFlightRequests) {
-            inFlightRequests.remove(this)
-        }
-
-        val frameNumber = FrameNumber(captureFrameNumber)
-        invokeOnRequests { request, _, listener ->
-            listener.onRequestSequenceCompleted(request, frameNumber)
-        }
-    }
-
-    override fun onCaptureSequenceAborted(
-        captureSession: CameraCaptureSession,
-        captureSequenceId: Int
-    ) {
-        check(sequenceNumber == captureSequenceId) {
-            "Abort was invoked on $sequenceNumber, but the sequence was not fully submitted!"
-        }
-
-        // Remove this request from the set of requests that are currently tracked.
-        synchronized(inFlightRequests) {
-            inFlightRequests.remove(this)
-        }
-
-        invokeOnRequests { request, _, listener ->
-            listener.onRequestSequenceAborted(request)
-        }
-    }
-
-    private fun readRequestNumber(request: CaptureRequest): RequestNumber =
-        checkNotNull(request.tag as RequestNumber)
-
-    private fun readRequest(requestNumber: RequestNumber): RequestInfo {
-        return checkNotNull(requests[requestNumber]) {
-            "Unable to find the request for $requestNumber!"
-        }
-    }
-
-    private inline fun invokeOnRequests(
-        crossinline fn: (RequestMetadata, Int, Request.Listener) -> Any
-    ) {
-
-        // Always invoke the internal listener first on all of the internal listeners for the
-        // entire sequence before invoking the listeners specified in the specific requests
-        for (i in captureRequests.indices) {
-            val requestNumber = readRequestNumber(captureRequests[i])
-            val request = checkNotNull(requests[requestNumber])
-
-            for (listenerIndex in internalListeners.indices) {
-                fn(request, i, internalListeners[listenerIndex])
-            }
-        }
-
-        for (i in captureRequests.indices) {
-            val requestNumber = readRequestNumber(captureRequests[i])
-            val request = checkNotNull(requests[requestNumber])
-
-            for (listenerIndex in request.request.listeners.indices) {
-                fn(request, i, request.request.listeners[listenerIndex])
-            }
-        }
-    }
-
-    private inline fun invokeOnRequest(
-        request: RequestInfo,
-        crossinline fn: (Request.Listener) -> Any
-    ) {
-        // Always invoke the internal listener first so that internal sate can be updated before
-        // other listeners ask for it.
-        for (i in internalListeners.indices) {
-            fn(internalListeners[i])
-        }
-
-        // Invoke the listeners that were defined on this request.
-        for (i in request.request.listeners.indices) {
-            fn(request.request.listeners[i])
-        }
-    }
-
-    override fun toString(): String = "CaptureSequence-$debugId"
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/SessionFactory.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/SessionFactory.kt
index 9663b3b..e1840e8 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/SessionFactory.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/SessionFactory.kt
@@ -23,10 +23,12 @@
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.core.Log
 import androidx.camera.camera2.pipe.wrapper.AndroidOutputConfiguration
 import androidx.camera.camera2.pipe.wrapper.CameraDeviceWrapper
 import androidx.camera.camera2.pipe.wrapper.InputConfigData
 import androidx.camera.camera2.pipe.wrapper.OutputConfigurationWrapper
+import androidx.camera.camera2.pipe.wrapper.OutputConfigurationWrapper.Companion.SURFACE_GROUP_ID_NONE
 import androidx.camera.camera2.pipe.wrapper.SessionConfigData
 import dagger.Module
 import dagger.Provides
@@ -64,7 +66,7 @@
             return androidPProvider.get()
         }
 
-        if (graphConfig.operatingMode == CameraGraph.OperatingMode.HIGH_SPEED) {
+        if (graphConfig.sessionMode == CameraGraph.OperatingMode.HIGH_SPEED) {
             check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                 "Cannot use HighSpeed sessions below Android M"
             }
@@ -82,7 +84,7 @@
         check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
             "CameraPipe is not supported below Android L"
         }
-        check(graphConfig.inputStream == null) {
+        check(graphConfig.input == null) {
             "Reprocessing is not supported on Android L"
         }
 
@@ -125,13 +127,14 @@
         surfaces: Map<StreamId, Surface>,
         virtualSessionState: VirtualSessionState
     ): Map<StreamId, OutputConfigurationWrapper> {
-        if (graphConfig.inputStream != null) {
+        if (graphConfig.input != null) {
             try {
+                val outputConfig = graphConfig.input.stream.outputs.single()
                 cameraDevice.createReprocessableCaptureSession(
                     InputConfiguration(
-                        graphConfig.inputStream.width,
-                        graphConfig.inputStream.height,
-                        graphConfig.inputStream.format
+                        outputConfig.size.width,
+                        outputConfig.size.height,
+                        outputConfig.format.value
                     ),
                     surfaces.map { it.value },
                     virtualSessionState,
@@ -178,7 +181,7 @@
 @RequiresApi(Build.VERSION_CODES.N)
 internal class AndroidNSessionFactory @Inject constructor(
     private val threads: Threads,
-    private val streamMap: StreamMap,
+    private val streamGraph: StreamGraphImpl,
     private val graphConfig: CameraGraph.Config
 ) : SessionFactory {
     override fun create(
@@ -188,23 +191,24 @@
     ): Map<StreamId, OutputConfigurationWrapper> {
         val outputs = buildOutputConfigurations(
             graphConfig,
-            streamMap,
+            streamGraph,
             surfaces
         )
 
         try {
-            if (graphConfig.inputStream == null) {
+            if (graphConfig.input == null) {
                 cameraDevice.createCaptureSessionByOutputConfigurations(
                     outputs.all,
                     virtualSessionState,
                     threads.camera2Handler
                 )
             } else {
+                val outputConfig = graphConfig.input.stream.outputs.single()
                 cameraDevice.createReprocessableCaptureSessionByConfigurations(
                     InputConfigData(
-                        graphConfig.inputStream.width,
-                        graphConfig.inputStream.height,
-                        graphConfig.inputStream.format
+                        outputConfig.size.width,
+                        outputConfig.size.height,
+                        outputConfig.format.value
                     ),
                     outputs.all,
                     virtualSessionState,
@@ -225,7 +229,7 @@
 internal class AndroidPSessionFactory @Inject constructor(
     private val threads: Threads,
     private val graphConfig: CameraGraph.Config,
-    private val streamMap: StreamMap
+    private val streamGraph: StreamGraphImpl
 ) : SessionFactory {
     override fun create(
         cameraDevice: CameraDeviceWrapper,
@@ -234,31 +238,34 @@
     ): Map<StreamId, OutputConfigurationWrapper> {
 
         val operatingMode =
-            when (graphConfig.operatingMode) {
-                CameraGraph.OperatingMode.NORMAL -> 0
-                CameraGraph.OperatingMode.HIGH_SPEED -> 1
+            when (graphConfig.sessionMode) {
+                CameraGraph.OperatingMode.NORMAL -> SessionConfigData.SESSION_TYPE_REGULAR
+                CameraGraph.OperatingMode.HIGH_SPEED -> SessionConfigData.SESSION_TYPE_HIGH_SPEED
             }
 
         val outputs = buildOutputConfigurations(
             graphConfig,
-            streamMap,
+            streamGraph,
             surfaces
         )
 
+        val input = graphConfig.input?.let {
+            val outputConfig = it.stream.outputs.single()
+            InputConfigData(
+                outputConfig.size.width,
+                outputConfig.size.height,
+                outputConfig.format.value
+            )
+        }
+
         val sessionConfig = SessionConfigData(
             operatingMode,
-            graphConfig.inputStream?.let {
-                InputConfigData(
-                    it.width,
-                    it.height,
-                    it.format
-                )
-            },
+            input,
             outputs.all,
             threads.camera2Executor,
             virtualSessionState,
-            graphConfig.template.value,
-            graphConfig.defaultParameters
+            graphConfig.sessionTemplate.value,
+            graphConfig.sessionParameters
         )
 
         try {
@@ -276,46 +283,77 @@
 @RequiresApi(Build.VERSION_CODES.N)
 internal fun buildOutputConfigurations(
     graphConfig: CameraGraph.Config,
-    streamMap: StreamMap,
+    streamGraph: StreamGraphImpl,
     surfaces: Map<StreamId, Surface>
 ): OutputConfigurations {
-    // TODO: Add support for:
-    //   surfaceGroupId
-    //   surfaceSharing
-    //   multipleSurfaces?
-
-    val outputs = arrayListOf<OutputConfigurationWrapper>()
+    val allOutputs = arrayListOf<OutputConfigurationWrapper>()
     val deferredOutputs = mutableMapOf<StreamId, OutputConfigurationWrapper>()
 
-    for (streamConfig in graphConfig.streams) {
-        val streamId = streamMap.streamConfigMap[streamConfig]!!.id
-        val physicalCameraId = if (streamConfig.camera != graphConfig.camera) {
-            streamConfig.camera
-        } else {
-            null
+    for (outputConfig in streamGraph.outputConfigs) {
+        val outputSurfaces = outputConfig.streams.mapNotNull { surfaces[it.id] }
+
+        val externalConfig = outputConfig.externalOutputConfig
+        if (externalConfig != null) {
+            check(outputSurfaces.size == outputConfig.streams.size) {
+                val missingStreams = outputConfig.streams.filter { !surfaces.contains(it.id) }
+                "Surfaces are not yet available for $outputConfig!" +
+                    " Missing surfaces for $missingStreams!"
+            }
+            allOutputs.add(
+                AndroidOutputConfiguration(
+                    externalConfig,
+                    surfaceSharing = false, // No way to read this value.
+                    maxSharedSurfaceCount = 1, // Hardcoded
+                    physicalCameraId = null, // No way to read this value.
+                )
+            )
+            continue
         }
 
-        val surface = surfaces[streamId]
+        if (outputConfig.deferrable && outputSurfaces.size != outputConfig.streams.size) {
+            val output = AndroidOutputConfiguration.create(
+                null,
+                size = outputConfig.size,
+                outputType = outputConfig.deferredOutputType!!,
+                surfaceSharing = outputConfig.surfaceSharing,
+                surfaceGroupId = outputConfig.groupNumber ?: SURFACE_GROUP_ID_NONE,
+                physicalCameraId = if (outputConfig.camera != graphConfig.camera) {
+                    outputConfig.camera
+                } else {
+                    null
+                }
+            )
+            allOutputs.add(output)
+            for (outputSurface in outputConfig.streamBuilder) {
+                deferredOutputs[outputSurface.id] = output
+            }
+            continue
+        }
 
-        val config = AndroidOutputConfiguration.create(
-            surface,
-            streamType = streamConfig.type,
-            size = streamConfig.size,
-            physicalCameraId = physicalCameraId
+        // Default case: We have the surface(s)
+        check(outputSurfaces.size == outputConfig.streams.size) {
+            val missingStreams = outputConfig.streams.filter { !surfaces.contains(it.id) }
+            "Surfaces are not yet available for $outputConfig!" +
+                " Missing surfaces for $missingStreams!"
+        }
+        val output = AndroidOutputConfiguration.create(
+            outputSurfaces.first(),
+            size = outputConfig.size,
+            surfaceSharing = outputConfig.surfaceSharing,
+            surfaceGroupId = outputConfig.groupNumber ?: SURFACE_GROUP_ID_NONE,
+            physicalCameraId = if (outputConfig.camera != graphConfig.camera) {
+                outputConfig.camera
+            } else {
+                null
+            }
         )
-
-        outputs.add(config)
-
-        if (surface == null) {
-            deferredOutputs[streamId] = config
+        for (surface in outputSurfaces.drop(1)) {
+            output.addSurface(surface)
         }
+        allOutputs.add(output)
     }
 
-    // TODO: Sort outputs by type to try and put the viewfinder output first in the list
-    //   This is important as some devices assume that the first surface is the viewfinder and
-    //   will treat it differently.
-
-    return OutputConfigurations(outputs, deferredOutputs)
+    return OutputConfigurations(allOutputs, deferredOutputs)
 }
 
 internal data class OutputConfigurations(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StandardRequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StandardRequestProcessor.kt
new file mode 100644
index 0000000..66cca5f
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StandardRequestProcessor.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2020 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.camera.camera2.pipe.impl
+
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CaptureRequest
+import android.util.ArrayMap
+import android.view.Surface
+import androidx.annotation.GuardedBy
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.Metadata
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.RequestTemplate
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.wrapper.CameraCaptureSessionWrapper
+import androidx.camera.camera2.pipe.wrapper.ObjectUnavailableException
+import androidx.camera.camera2.pipe.writeParameters
+import kotlinx.atomicfu.atomic
+import java.util.Collections.singletonList
+import java.util.Collections.singletonMap
+import javax.inject.Inject
+
+internal interface RequestProcessorFactory {
+    fun create(
+        session: CameraCaptureSessionWrapper,
+        surfaceMap: Map<StreamId, Surface>
+    ): RequestProcessor
+}
+
+internal class StandardRequestProcessorFactory @Inject constructor(
+    private val threads: Threads,
+    private val graphConfig: CameraGraph.Config,
+    @ForCameraGraph private val graphListeners: ArrayList<Request.Listener>,
+    private val graphState3A: GraphState3A
+) : RequestProcessorFactory {
+    override fun create(
+        session: CameraCaptureSessionWrapper,
+        surfaceMap: Map<StreamId, Surface>
+    ): RequestProcessor =
+        StandardRequestProcessor(
+            session,
+            threads,
+            graphConfig,
+            surfaceMap,
+            graphListeners,
+            graphState3A
+        )
+}
+
+internal val requestProcessorDebugIds = atomic(0)
+internal val requestSequenceDebugIds = atomic(0L)
+internal val requestTags = atomic(0L)
+internal fun nextRequestTag(): RequestNumber = RequestNumber(requestTags.incrementAndGet())
+
+/**
+ * This class is designed to synchronously handle interactions with the Camera CaptureSession.
+ */
+internal class StandardRequestProcessor(
+    private val session: CameraCaptureSessionWrapper,
+    private val threads: Threads,
+    private val graphConfig: CameraGraph.Config,
+    private val surfaceMap: Map<StreamId, Surface>,
+    private val graphListeners: List<Request.Listener>,
+    private val graphState3A: GraphState3A
+) : RequestProcessor {
+
+    @GuardedBy("inFlightRequests")
+    private val inFlightRequests = mutableListOf<CaptureSequence>()
+    private val debugId = requestProcessorDebugIds.incrementAndGet()
+    private val closed = atomic(false)
+
+    override fun submit(
+        request: Request,
+        defaultParameters: Map<*, Any>,
+        requiredParameters: Map<*, Any>
+    ): Boolean {
+        return configureAndCapture(
+            singletonList(request),
+            defaultParameters,
+            requiredParameters,
+            requireStreams = false,
+            isRepeating = false
+        )
+    }
+
+    override fun submit(
+        requests: List<Request>,
+        defaultParameters: Map<*, Any>,
+        requiredParameters: Map<*, Any>
+    ): Boolean {
+        return configureAndCapture(
+            requests,
+            defaultParameters,
+            requiredParameters,
+            requireStreams = false,
+            isRepeating = false
+        )
+    }
+
+    override fun setRepeating(
+        request: Request,
+        defaultParameters: Map<*, Any>,
+        requiredParameters: Map<*, Any>
+    ): Boolean {
+        return configureAndCapture(
+            singletonList(request),
+            defaultParameters,
+            requiredParameters,
+            requireStreams = false,
+            isRepeating = true
+        )
+    }
+
+    override fun abortCaptures() {
+        val requestsToAbort = synchronized(inFlightRequests) {
+            val copy = inFlightRequests.toList()
+            inFlightRequests.clear()
+            copy
+        }
+        for (sequence in requestsToAbort) {
+            sequence.invokeOnAborted()
+        }
+        session.abortCaptures()
+    }
+
+    override fun stopRepeating() {
+        session.stopRepeating()
+    }
+
+    override fun close() {
+        closed.compareAndSet(expect = false, update = true)
+    }
+
+    private fun configureAndCapture(
+        requests: List<Request>,
+        defaultParameters: Map<*, Any>,
+        requiredParameters: Map<*, Any>,
+        requireStreams: Boolean,
+        isRepeating: Boolean
+    ): Boolean {
+        // Reject incoming requests if this instance has been stopped or closed.
+        if (closed.value) {
+            return false
+        }
+
+        val requestMap = ArrayMap<RequestNumber, RequestInfo>(requests.size)
+        val captureRequests = ArrayList<CaptureRequest>(requests.size)
+
+        val surfaceToStreamMap = ArrayMap<Surface, StreamId>()
+        val streamToSurfaceMap = ArrayMap<StreamId, Surface>()
+
+        for (request in requests) {
+            val requestTemplate = request.template ?: graphConfig.defaultTemplate
+
+            Log.debug { "Building CaptureRequest for $request" }
+
+            // Check to see if there is at least one valid surface for each stream.
+            var hasSurface = false
+            for (stream in request.streams) {
+                if (streamToSurfaceMap.contains(stream)) {
+                    hasSurface = true
+                    continue
+                }
+
+                val surface = surfaceMap[stream]
+                if (surface != null) {
+                    Log.debug { "  Binding $stream to $surface" }
+
+                    // TODO(codelogic) There should be a more efficient way to do these lookups than
+                    // having two maps.
+                    surfaceToStreamMap[surface] = stream
+                    streamToSurfaceMap[stream] = surface
+                    hasSurface = true
+                } else if (requireStreams) {
+                    Log.info { "  Failed to bind surface to $stream" }
+                    // If requireStreams is set we are required to map every stream to a valid
+                    // Surface object for this request. If this condition is violated, then we
+                    // return false because we cannot submit these request(s) until there is a valid
+                    // StreamId -> Surface mapping for all streams.
+                    return false
+                }
+            }
+
+            // If there are no surfaces on a particular request, camera2 will now allow us to
+            // submit it.
+            if (!hasSurface) {
+                return false
+            }
+
+            // Create the request builder. There is a risk this will throw an exception or return null
+            // if the CameraDevice has been closed or disconnected. If this fails, indicate that the
+            // request was not submitted.
+            val requestBuilder: CaptureRequest.Builder
+            try {
+                requestBuilder = session.device.createCaptureRequest(requestTemplate)
+            } catch (exception: ObjectUnavailableException) {
+                return false
+            }
+
+            // Apply the output surfaces to the requestBuilder
+            hasSurface = false
+            for (stream in request.streams) {
+                val surface = streamToSurfaceMap[stream]
+                if (surface != null) {
+                    requestBuilder.addTarget(surface)
+                    hasSurface = true
+                }
+            }
+
+            // Soundness check to make sure we add at least one surface. This should be guaranteed
+            // because we are supposed to exit early and return false if we cannot map at least one
+            // surface per request.
+            check(hasSurface)
+
+            // Apply default parameters to the request builder first.
+            requestBuilder.writeParameters(defaultParameters)
+
+            // Apply request parameters to the request builder.
+            requestBuilder.writeParameters(request.parameters)
+
+            // Apply the 3A parameters. This gives the users of camerapipe the ability to
+            // still override the 3A parameters for complicated use cases.
+            //
+            // TODO(sushilnath@): Implement one of the two options. (1) Apply the 3A parameters
+            // from internal 3A state machine at last and provide a flag in the Request object to
+            // specify when the clients want to explicitly override some of the 3A parameters
+            // directly. Add code to handle the flag. (2) Let clients override the 3A parameters
+            // freely and when that happens intercept those parameters from the request and keep the
+            // internal 3A state machine in sync.
+            graphState3A.writeTo(requestBuilder)
+
+            // Finally, write required parameters to the request builder. This will override any
+            // value that has ben previously set.
+            requestBuilder.writeParameters(requiredParameters)
+
+            // The tag must be set for every request. We use it to lookup listeners for the
+            // individual requests so that each request can specify individual listeners.
+            val requestTag = nextRequestTag()
+            requestBuilder.setTag(requestTag)
+
+            // Create the camera2 captureRequest and add it to our list of requests.
+            val captureRequest = requestBuilder.build()
+            captureRequests.add(captureRequest)
+
+            @Suppress("SyntheticAccessor")
+            requestMap[requestTag] = RequestInfo(
+                captureRequest,
+                defaultParameters,
+                requiredParameters,
+                streamToSurfaceMap,
+                requestTemplate,
+                isRepeating,
+                request,
+                requestTag
+            )
+        }
+
+        // Create the captureSequence listener
+        @Suppress("SyntheticAccessor")
+        val captureSequence = CaptureSequence(
+            graphListeners,
+            if (requests.size == 1) {
+                singletonMap(requestMap.keyAt(0), requestMap.valueAt(0))
+            } else {
+                requestMap
+            },
+            captureRequests,
+            surfaceToStreamMap,
+            inFlightRequests,
+            session.device.cameraId
+        )
+
+        // Non-repeating requests must always be aware of abort calls.
+        if (!isRepeating) {
+            synchronized(inFlightRequests) {
+                inFlightRequests.add(captureSequence)
+            }
+        }
+
+        var captured = false
+        return try {
+            Log.debug { "Submitting $captureSequence" }
+            capture(captureRequests, captureSequence, isRepeating)
+            captured = true
+            Log.debug { "Submitted $captureSequence" }
+            true
+        } catch (closedException: ObjectUnavailableException) {
+            false
+        } catch (accessException: CameraAccessException) {
+            false
+        } finally {
+            // If ANY unhandled exception occurs, don't throw, but make sure we remove it from the
+            // list of in-flight requests.
+            if (!captured && !isRepeating) {
+                captureSequence.invokeOnAborted()
+            }
+        }
+    }
+
+    private fun capture(
+        captureRequests: List<CaptureRequest>,
+        captureSequence: CaptureSequence,
+        isRepeating: Boolean
+    ) {
+        captureSequence.invokeOnRequestSequenceCreated()
+
+        // NOTE: This is a funny synchronization call. The purpose is to avoid a rare but possible
+        // situation where calling capture causes one of the callback methods to be invoked before
+        // sequenceNumber has been set on the callback. Both this call and the synchronized
+        // behavior on the CaptureSequence listener have been designed to minimize the number of
+        // synchronized calls.
+        synchronized(lock = captureSequence) {
+            // TODO: Update these calls to use executors on newer versions of the OS
+            val sequenceNumber: Int = if (captureRequests.size == 1) {
+                if (isRepeating) {
+                    session.setRepeatingRequest(
+                        captureRequests[0],
+                        captureSequence,
+                        threads.camera2Handler
+                    )
+                } else {
+                    session.capture(captureRequests[0], captureSequence, threads.camera2Handler)
+                }
+            } else {
+                if (isRepeating) {
+                    session.setRepeatingBurst(
+                        captureRequests,
+                        captureSequence,
+                        threads.camera2Handler
+                    )
+                } else {
+                    session.captureBurst(captureRequests, captureSequence, threads.camera2Handler)
+                }
+            }
+            captureSequence.sequenceNumber = sequenceNumber
+        }
+
+        // Invoke callbacks without holding a lock.
+        captureSequence.invokeOnRequestSequenceSubmitted()
+    }
+
+    override fun toString(): String {
+        return "RequestProcessor-$debugId"
+    }
+}
+
+/**
+ * This class packages together information about a request that was submitted to the camera.
+ */
+@Suppress("SyntheticAccessor") // Using an inline class generates a synthetic constructor
+internal class RequestInfo(
+    private val captureRequest: CaptureRequest,
+    private val defaultParameters: Map<*, Any>,
+    private val requiredParameters: Map<*, Any>,
+    override val streams: Map<StreamId, Surface>,
+    override val template: RequestTemplate,
+    override val repeating: Boolean,
+    override val request: Request,
+    override val requestNumber: RequestNumber
+) : RequestMetadata {
+    override fun <T> get(key: CaptureRequest.Key<T>): T? = captureRequest[key]
+    override fun <T> getOrDefault(key: CaptureRequest.Key<T>, default: T): T =
+        get(key) ?: default
+
+    @Suppress("UNCHECKED_CAST")
+    override fun <T> get(key: Metadata.Key<T>): T? =
+        (requiredParameters[key] ?: request.extras[key] ?: defaultParameters[key]) as T?
+
+    override fun <T> getOrDefault(key: Metadata.Key<T>, default: T): T = get(key) ?: default
+
+    override fun unwrap(): CaptureRequest = captureRequest
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StreamGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StreamGraphImpl.kt
new file mode 100644
index 0000000..cc8aabe
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StreamGraphImpl.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2020 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.camera.camera2.pipe.impl
+
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+import android.hardware.camera2.params.OutputConfiguration
+import android.os.Build
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraStream
+import androidx.camera.camera2.pipe.InputStream
+import androidx.camera.camera2.pipe.OutputId
+import androidx.camera.camera2.pipe.OutputStream
+import androidx.camera.camera2.pipe.OutputStream.Config.ExternalOutputConfig
+import androidx.camera.camera2.pipe.OutputStream.Config.LazyOutputConfig
+import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.StreamGraph
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.wrapper.Api24Compat
+import kotlinx.atomicfu.atomic
+import javax.inject.Inject
+
+private val streamIds = atomic(0)
+internal fun nextStreamId(): StreamId = StreamId(streamIds.incrementAndGet())
+
+private val outputIds = atomic(0)
+internal fun nextOutputId(): OutputId = OutputId(outputIds.incrementAndGet())
+
+private val configIds = atomic(0)
+internal fun nextConfigId(): CameraConfigId = CameraConfigId(configIds.incrementAndGet())
+
+private val groupIds = atomic(0)
+internal fun nextGroupId(): Int = groupIds.incrementAndGet()
+
+@Suppress("EXPERIMENTAL_FEATURE_WARNING")
+internal inline class CameraConfigId(val value: Int) {
+    override fun toString(): String = "OutputConfig-$value"
+}
+
+/**
+ * This object keeps track of which surfaces have been configured for each stream. In addition,
+ * it will keep track of which surfaces have changed or replaced so that the CaptureSession can be
+ * reconfigured if the configured surfaces change.
+ */
+@CameraGraphScope
+internal class StreamGraphImpl @Inject constructor(
+    cameraMetadata: CameraMetadata,
+    graphConfig: CameraGraph.Config
+) : StreamGraph {
+    private val surfaceMap: MutableMap<StreamId, Surface> = mutableMapOf()
+    private val _streamMap: Map<CameraStream.Config, CameraStream>
+
+    internal val outputConfigs: List<OutputConfig>
+
+    // TODO: Build InputStream(s)
+    override val input: InputStream? = null
+    override val streams: List<CameraStream>
+    override val outputs: List<OutputStream>
+
+    override fun get(config: CameraStream.Config): CameraStream? = _streamMap[config]
+
+    init {
+        val outputConfigListBuilder = mutableListOf<OutputConfig>()
+        val outputConfigMap = mutableMapOf<OutputStream.Config, OutputConfig>()
+
+        val streamListBuilder = mutableListOf<CameraStream>()
+        val streamMapBuilder = mutableMapOf<CameraStream.Config, CameraStream>()
+
+        val deferredOutputsAllowed = computeIfDeferredStreamsAreSupported(
+            cameraMetadata,
+            graphConfig
+        )
+
+        // Compute groupNumbers for buffer sharing.
+        val groupNumbers = mutableMapOf<CameraStream.Config, Int>()
+        for (group in graphConfig.streamSharingGroups) {
+            check(group.size > 1)
+            val surfaceGroupId = computeNextSurfaceGroupId(graphConfig)
+            for (config in group) {
+                check(!groupNumbers.containsKey(config))
+                groupNumbers[config] = surfaceGroupId
+            }
+        }
+
+        // Create outputConfigs. If outputs are shared there can be fewer entries in map than there
+        // are streams.
+        for (streamConfig in graphConfig.streams) {
+            for (output in streamConfig.outputs) {
+                if (outputConfigMap.containsKey(output)) {
+                    continue
+                }
+
+                @SuppressWarnings("SyntheticAccessor")
+                val outputConfig = OutputConfig(
+                    nextConfigId(),
+                    output.size,
+                    output.format,
+                    output.camera ?: graphConfig.camera,
+                    groupNumber = groupNumbers[streamConfig],
+                    deferredOutputType = if (deferredOutputsAllowed) {
+                        (output as? LazyOutputConfig)?.outputType
+                    } else {
+                        null
+                    },
+                    externalOutputConfig = (output as? ExternalOutputConfig)?.output
+                )
+                outputConfigMap[output] = outputConfig
+                outputConfigListBuilder.add(outputConfig)
+            }
+        }
+
+        // Build the streams
+        for (streamConfigIdx in graphConfig.streams.indices) {
+            val streamConfig = graphConfig.streams[streamConfigIdx]
+
+            val outputs = streamConfig.outputs.map {
+                val outputConfig = outputConfigMap[it]!!
+
+                @SuppressWarnings("SyntheticAccessor")
+                val outputStream = OutputStreamImpl(
+                    nextOutputId(),
+                    outputConfig.size,
+                    outputConfig.format,
+                    outputConfig.camera
+                )
+                outputStream
+            }
+
+            val stream = CameraStream(nextStreamId(), outputs)
+            streamMapBuilder[streamConfig] = stream
+            streamListBuilder.add(stream)
+            for (output in outputs) {
+                output.stream = stream
+            }
+            for (cameraOutputConfig in streamConfig.outputs) {
+                outputConfigMap[cameraOutputConfig]!!.streamBuilder.add(stream)
+            }
+        }
+
+        // TODO: Sort outputs by type to try and put the viewfinder output first in the list
+        //   This is important as some devices assume that the first surface is the viewfinder and
+        //   will treat it differently.
+
+        streams = streamListBuilder
+        _streamMap = streamMapBuilder
+        outputs = streams.flatMap { it.outputs }
+        outputConfigs = outputConfigListBuilder
+    }
+
+    private var _listener: SurfaceListener? = null
+    var listener: SurfaceListener?
+        get() = _listener
+        set(value) {
+            _listener = value
+            if (value != null) {
+                maybeUpdateSurfaces()
+            }
+        }
+
+    operator fun set(stream: StreamId, surface: Surface?) {
+        Log.info {
+            if (surface != null) {
+                "Configured $stream to use $surface"
+            } else {
+                "Removed surface for $stream"
+            }
+        }
+        if (surface == null) {
+            // TODO: Tell the graph processor that it should resubmit the repeating request or
+            //  reconfigure the camera2 captureSession
+            surfaceMap.remove(stream)
+        } else {
+            surfaceMap[stream] = surface
+        }
+        maybeUpdateSurfaces()
+    }
+
+    private fun maybeUpdateSurfaces() {
+        val surfaceListener = _listener ?: return
+
+        // Rules:
+        // 1. In order to tell the captureSession that we have surfaces, we should wait until we
+        //    have at least one valid surface.
+        // 2. All streams that are not deferrable, must have a valid surface.
+
+        val surfaces = mutableMapOf<StreamId, Surface>()
+        for (outputConfig in outputConfigs) {
+            for (stream in outputConfig.streamBuilder) {
+                val surface = surfaceMap[stream.id]
+                if (surface == null) {
+                    if (!outputConfig.deferrable) {
+                        return
+                    }
+                } else {
+                    surfaces[stream.id] = surface
+                }
+            }
+        }
+
+        if (surfaces.isEmpty()) {
+            return
+        }
+
+        surfaceListener.onSurfaceMapUpdated(surfaces)
+    }
+
+    @Suppress("SyntheticAccessor") // StreamId generates a synthetic constructor
+    class OutputConfig(
+        val id: CameraConfigId,
+        val size: Size,
+        val format: StreamFormat,
+        val camera: CameraId,
+        val groupNumber: Int?,
+        val externalOutputConfig: OutputConfiguration?,
+        val deferredOutputType: OutputStream.OutputType?,
+    ) {
+        internal val streamBuilder = mutableListOf<CameraStream>()
+        val streams: List<CameraStream>
+            get() = streamBuilder
+        val deferrable: Boolean
+            get() = deferredOutputType != null
+        val surfaceSharing = streamBuilder.size > 1
+        override fun toString(): String = id.toString()
+    }
+
+    @Suppress("SyntheticAccessor") // OutputId generates a synthetic constructor
+    private class OutputStreamImpl(
+        override val id: OutputId,
+        override val size: Size,
+        override val format: StreamFormat,
+        override val camera: CameraId,
+    ) : OutputStream {
+        override lateinit var stream: CameraStream
+        override fun toString(): String = id.toString()
+    }
+
+    interface SurfaceListener {
+        fun onSurfaceMapUpdated(surfaces: Map<StreamId, Surface>)
+    }
+
+    private fun computeNextSurfaceGroupId(graphConfig: CameraGraph.Config): Int {
+        // If there are any existing surfaceGroups, make sure the groups we define do not overlap
+        // with any existing values.
+        val existingGroupNumbers: List<Int> = readExistingGroupNumbers(graphConfig.streams)
+
+        // Loop until we produce a groupId that was not already used.
+        var number = nextGroupId()
+        while (existingGroupNumbers.contains(number)) {
+            number = nextGroupId()
+        }
+        return number
+    }
+
+    private fun readExistingGroupNumbers(outputs: List<CameraStream.Config>): List<Int> {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            outputs
+                .flatMap { it.outputs }
+                .filterIsInstance<ExternalOutputConfig>()
+                .fold(mutableListOf()) { values, config ->
+                    val groupId = Api24Compat.getSurfaceGroupId(config.output)
+                    if (!values.contains(groupId)) {
+                        values.add(groupId)
+                    }
+                    values
+                }
+        } else {
+            emptyList()
+        }
+    }
+
+    private fun computeIfDeferredStreamsAreSupported(
+        cameraMetadata: CameraMetadata,
+        graphConfig: CameraGraph.Config
+    ): Boolean {
+        val hardwareLevel = cameraMetadata[INFO_SUPPORTED_HARDWARE_LEVEL]
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
+            graphConfig.sessionMode == CameraGraph.OperatingMode.NORMAL &&
+            hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY &&
+            hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED &&
+            (
+                Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
+                    hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
+                )
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StreamMap.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StreamMap.kt
deleted file mode 100644
index 8f67b7a..0000000
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StreamMap.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright 2020 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.camera.camera2.pipe.impl
-
-import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
-import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
-import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
-import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
-import android.os.Build
-import android.util.Size
-import android.view.Surface
-import androidx.camera.camera2.pipe.CameraGraph
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.Stream
-import androidx.camera.camera2.pipe.StreamConfig
-import androidx.camera.camera2.pipe.StreamFormat
-import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.StreamType
-import kotlinx.atomicfu.atomic
-import javax.inject.Inject
-
-private val streamIds = atomic(0)
-internal fun nextStreamId(): StreamId = StreamId(streamIds.incrementAndGet())
-
-/**
- * This object keeps track of which surfaces have been configured for each stream. In addition,
- * it will keep track of which surfaces have changed or replaced so that the CaptureSession can be
- * reconfigured if the configured surfaces change.
- */
-@CameraGraphScope
-internal class StreamMap @Inject constructor(
-    cameraMetadata: CameraMetadata,
-    graphConfig: CameraGraph.Config
-) {
-    private val surfaceMap: MutableMap<StreamId, Surface> = mutableMapOf()
-    private val deferrableStreams: Set<StreamId>
-
-    val streamConfigMap: Map<StreamConfig, Stream>
-
-    init {
-        val streamBuilder = mutableMapOf<StreamConfig, Stream>()
-        val deferrableStreamBuilder = mutableSetOf<StreamId>()
-
-        val hardwareLevel = cameraMetadata[INFO_SUPPORTED_HARDWARE_LEVEL]
-        val deferredStreamsSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
-            graphConfig.operatingMode == CameraGraph.OperatingMode.NORMAL &&
-            hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY &&
-            hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED &&
-            (
-                Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
-                    hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
-                )
-
-        for (streamConfig in graphConfig.streams) {
-            // Using an inline class generates a synthetic constructor
-            @Suppress("SyntheticAccessor")
-            val stream = StreamImpl(
-                nextStreamId(),
-                streamConfig.size,
-                streamConfig.format,
-                streamConfig.camera,
-                streamConfig.type
-            )
-
-            streamBuilder[streamConfig] = stream
-
-            if (deferredStreamsSupported &&
-                streamConfig.deferrable &&
-                (
-                    streamConfig.type == StreamType.SURFACE_TEXTURE ||
-                        streamConfig.type == StreamType.SURFACE_VIEW
-                    )
-            ) {
-                deferrableStreamBuilder.add(stream.id)
-            }
-        }
-        deferrableStreams = deferrableStreamBuilder
-        streamConfigMap = streamBuilder
-    }
-
-    private var _listener: SurfaceListener? = null
-    var listener: SurfaceListener?
-        get() = _listener
-        set(value) {
-            _listener = value
-            if (value != null) {
-                maybeUpdateSurfaces()
-            }
-        }
-
-    operator fun set(stream: StreamId, surface: Surface?) {
-        Log.info {
-            if (surface != null) {
-                "Configured $stream to use $surface"
-            } else {
-                "Removed surface for $stream"
-            }
-        }
-        if (surface == null) {
-            // TODO: Tell the graph processor that it should resubmit the repeating request or
-            //  reconfigure the camera2 captureSession
-            surfaceMap.remove(stream)
-        } else {
-            surfaceMap[stream] = surface
-        }
-        maybeUpdateSurfaces()
-    }
-
-    private fun maybeUpdateSurfaces() {
-        val surfaceListener = _listener ?: return
-
-        // Rules:
-        // 1. In order to tell the captureSession that we have surfaces, we should wait until we
-        //    have at least one valid surface.
-        // 2. All streams that are not deferrable, must have a valid surface.
-
-        val surfaces = mutableMapOf<StreamId, Surface>()
-        for (stream in streamConfigMap) {
-            val surface = surfaceMap[stream.value.id]
-
-            if (surface == null) {
-                if (!deferrableStreams.contains(stream.value.id)) {
-                    // Break early if no surface is defined, and the stream is not deferrable.
-                    return
-                }
-            } else {
-                surfaces[stream.value.id] = surface
-            }
-        }
-
-        if (surfaces.isEmpty()) {
-            return
-        }
-
-        surfaceListener.setSurfaceMap(surfaces)
-    }
-
-    // Using an inline class generates a synthetic constructor
-    @Suppress("SyntheticAccessor")
-    data class StreamImpl(
-        override val id: StreamId,
-        override val size: Size,
-        override val format: StreamFormat,
-        override val camera: CameraId,
-        override val type: StreamType
-    ) : Stream {
-        override fun toString(): String = id.toString()
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCamera.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCamera.kt
index 78c4b50..b25a0ec73 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCamera.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCamera.kt
@@ -22,7 +22,12 @@
 import androidx.annotation.GuardedBy
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.DurationNs
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.TimestampNs
+import androidx.camera.camera2.pipe.core.Timestamps
+import androidx.camera.camera2.pipe.core.Timestamps.formatMs
 import androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice
 import androidx.camera.camera2.pipe.wrapper.CameraDeviceWrapper
 import androidx.camera.camera2.pipe.wrapper.closeWithTrace
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCameraManager.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCameraManager.kt
index 956758ee..662780a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCameraManager.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCameraManager.kt
@@ -20,6 +20,9 @@
 import android.hardware.camera2.CameraManager
 import android.os.Build
 import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.Timestamps
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualSessionState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualSessionState.kt
index 43644b1..118d015 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualSessionState.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualSessionState.kt
@@ -17,10 +17,13 @@
 package androidx.camera.camera2.pipe.impl
 
 import android.view.Surface
-
 import androidx.annotation.GuardedBy
 import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.TimestampNs
+import androidx.camera.camera2.pipe.core.Timestamps
+import androidx.camera.camera2.pipe.core.Timestamps.formatMs
 import androidx.camera.camera2.pipe.wrapper.CameraCaptureSessionWrapper
 import androidx.camera.camera2.pipe.wrapper.CameraDeviceWrapper
 import androidx.camera.camera2.pipe.wrapper.OutputConfigurationWrapper
@@ -29,10 +32,6 @@
 import kotlinx.coroutines.launch
 import java.util.Collections.synchronizedMap
 
-internal interface SurfaceListener {
-    fun setSurfaceMap(surfaces: Map<StreamId, Surface>)
-}
-
 internal val virtualSessionDebugIds = atomic(0)
 
 /**
@@ -46,9 +45,9 @@
 internal class VirtualSessionState(
     private val graphProcessor: GraphProcessor,
     private val sessionFactory: SessionFactory,
-    private val requestProcessorFactory: RequestProcessor.Factory,
+    private val requestProcessorFactory: RequestProcessorFactory,
     private val scope: CoroutineScope
-) : CameraCaptureSessionWrapper.StateCallback, SurfaceListener {
+) : CameraCaptureSessionWrapper.StateCallback, StreamGraphImpl.SurfaceListener {
     private val debugId = virtualSessionDebugIds.incrementAndGet()
     private val lock = Any()
 
@@ -93,7 +92,7 @@
 
     @GuardedBy("lock")
     private var _surfaceMap: Map<StreamId, Surface>? = null
-    override fun setSurfaceMap(surfaces: Map<StreamId, Surface>) {
+    override fun onSurfaceMapUpdated(surfaces: Map<StreamId, Surface>) {
         synchronized(lock) {
             if (state == State.CLOSING || state == State.CLOSED) {
                 return@synchronized
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/ApiCompat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/ApiCompat.kt
new file mode 100644
index 0000000..bfe6428
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/ApiCompat.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 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.camera.camera2.pipe.wrapper
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.hardware.camera2.params.OutputConfiguration
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+@RequiresApi(Build.VERSION_CODES.N)
+internal object Api24Compat {
+    @JvmStatic
+    fun getSurfaceGroupId(outputConfiguration: OutputConfiguration): Int {
+        return outputConfiguration.surfaceGroupId
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.P)
+internal object Api28Compat {
+    @JvmStatic
+    fun getAvailablePhysicalCameraRequestKeys(
+        cameraCharacteristics: CameraCharacteristics
+    ): List<CaptureRequest.Key<*>>? {
+        return cameraCharacteristics.availablePhysicalCameraRequestKeys
+    }
+
+    @JvmStatic
+    fun getAvailableSessionKeys(
+        cameraCharacteristics: CameraCharacteristics
+    ): List<CaptureRequest.Key<*>>? {
+        return cameraCharacteristics.availableSessionKeys
+    }
+
+    @JvmStatic
+    fun getPhysicalCaptureResults(
+        totalCaptureResult: TotalCaptureResult
+    ): Map<String, CaptureResult>? {
+        return totalCaptureResult.physicalCameraResults
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt
index 3e80b18..362db5f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt
@@ -30,10 +30,10 @@
 import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.UnsafeWrapper
-import androidx.camera.camera2.pipe.impl.Debug
-import androidx.camera.camera2.pipe.impl.Log
-import androidx.camera.camera2.pipe.impl.Timestamps
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.Timestamps
+import androidx.camera.camera2.pipe.core.Timestamps.formatMs
 import androidx.camera.camera2.pipe.writeParameter
 import kotlin.jvm.Throws
 
@@ -248,7 +248,7 @@
         // Iterate template parameters and CHECK BY NAME, as there have been cases where equality
         // checks did not pass.
         for ((key, value) in config.sessionParameters) {
-            if (key is CaptureRequest.Key<*> && sessionKeyNames.contains(key.name)) {
+            if (sessionKeyNames.contains(key.name)) {
                 requestBuilder.writeParameter(key, value)
             }
         }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CaptureSession.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CaptureSession.kt
index db12af07..f0daa8c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CaptureSession.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CaptureSession.kt
@@ -25,7 +25,7 @@
 import android.view.Surface
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.UnsafeWrapper
-import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.core.Log
 import kotlinx.atomicfu.atomic
 import java.io.Closeable
 import kotlin.jvm.Throws
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Configuration.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Configuration.kt
index 604ea45..0352257 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Configuration.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Configuration.kt
@@ -18,6 +18,7 @@
 
 import android.annotation.SuppressLint
 import android.graphics.SurfaceTexture
+import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.params.OutputConfiguration
 import android.os.Build
 import android.util.Size
@@ -25,11 +26,11 @@
 import android.view.SurfaceHolder
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.StreamType
+import androidx.camera.camera2.pipe.OutputStream.OutputType
 import androidx.camera.camera2.pipe.UnsafeWrapper
-import androidx.camera.camera2.pipe.impl.checkNOrHigher
-import androidx.camera.camera2.pipe.impl.checkOOrHigher
-import androidx.camera.camera2.pipe.impl.checkPOrHigher
+import androidx.camera.camera2.pipe.core.checkNOrHigher
+import androidx.camera.camera2.pipe.core.checkOOrHigher
+import androidx.camera.camera2.pipe.core.checkPOrHigher
 import androidx.camera.camera2.pipe.wrapper.OutputConfigurationWrapper.Companion.SURFACE_GROUP_ID_NONE
 import java.util.concurrent.Executor
 
@@ -46,7 +47,7 @@
     val stateCallback: CameraCaptureSessionWrapper.StateCallback,
 
     val sessionTemplateId: Int,
-    val sessionParameters: Map<*, Any>
+    val sessionParameters: Map<CaptureRequest.Key<*>, Any>
 ) {
     companion object {
         /* NOTE: These must keep in sync with their SessionConfiguration values. */
@@ -132,7 +133,7 @@
          */
         fun create(
             surface: Surface?,
-            streamType: StreamType = StreamType.SURFACE,
+            outputType: OutputType = OutputType.SURFACE,
             size: Size? = null,
             surfaceSharing: Boolean = false,
             surfaceGroupId: Int = SURFACE_GROUP_ID_NONE,
@@ -142,7 +143,11 @@
 
             // Create the OutputConfiguration using the groupId via the constructor (if set)
             val configuration: OutputConfiguration
-            if (surface != null) {
+            if (outputType == OutputType.SURFACE) {
+                check(surface != null) {
+                    "OutputConfigurations defined with ${OutputType.SURFACE} must provide a valid" +
+                        " surface!"
+                }
                 configuration = if (surfaceGroupId != SURFACE_GROUP_ID_NONE) {
                     OutputConfiguration(surfaceGroupId, surface)
                 } else {
@@ -159,17 +164,14 @@
                 check(size != null) {
                     "Size must defined when creating a deferred OutputConfiguration."
                 }
-
-                configuration = OutputConfiguration(
-                    size,
-                    when (streamType) {
-                        StreamType.SURFACE_TEXTURE -> SurfaceTexture::class.java
-                        StreamType.SURFACE_VIEW -> SurfaceHolder::class.java
-                        StreamType.SURFACE -> throw IllegalArgumentException(
-                            "StreamType.Surface is not supported for deferred OutputConfigurations"
-                        )
-                    }
-                )
+                val outputKlass = when (outputType) {
+                    OutputType.SURFACE_TEXTURE -> SurfaceTexture::class.java
+                    OutputType.SURFACE_VIEW -> SurfaceHolder::class.java
+                    OutputType.SURFACE -> throw IllegalStateException(
+                        "Unsupported OutputType: $outputType"
+                    )
+                }
+                configuration = OutputConfiguration(size, outputKlass)
             }
 
             // Enable surface sharing, if set.
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Exceptions.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Exceptions.kt
index 60b5f60..f335a96 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Exceptions.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Exceptions.kt
@@ -17,7 +17,7 @@
 package androidx.camera.camera2.pipe.wrapper
 
 import android.hardware.camera2.CameraAccessException
-import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.core.Log
 import kotlin.jvm.Throws
 
 /**
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
index 116f43a..246802dd 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
@@ -48,7 +48,7 @@
             CameraGraph.Config(
                 camera = fakeCameraId,
                 streams = listOf(),
-                template = RequestTemplate(0)
+                defaultTemplate = RequestTemplate(0)
             )
         )
         assertThat(cameraGraph).isNotNull()
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
index 338c160..5e02b30 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
@@ -33,8 +33,8 @@
     fun requestHasDefaults() {
         val request = Request(listOf(StreamId(1)))
 
-        assertThat(request.requestParameters).isEmpty()
-        assertThat(request.extraRequestParameters).isEmpty()
+        assertThat(request.parameters).isEmpty()
+        assertThat(request.extras).isEmpty()
         assertThat(request.template).isNull()
         assertThat(request.listeners).isEmpty()
 
@@ -45,10 +45,10 @@
     fun canReadCaptureParameters() {
         val request = Request(
             listOf(StreamId(1)),
-            requestParameters = mapOf(
+            parameters = mapOf(
                 CaptureRequest.EDGE_MODE to CaptureRequest.EDGE_MODE_HIGH_QUALITY
             ),
-            extraRequestParameters = mapOf(FakeMetadata.TEST_KEY to 42)
+            extras = mapOf(FakeMetadata.TEST_KEY to 42)
         )
 
         // Check with a valid test key
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt
index 3f935c0..5475134 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt
@@ -27,37 +27,50 @@
 @RunWith(CameraPipeRobolectricTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class StreamTest {
-
-    private val streamConfig1 = StreamConfig(
+    private val streamConfig1 = CameraStream.Config.create(
         size = Size(640, 480),
-        format = StreamFormat.YUV_420_888,
-        camera = CameraId("test"),
-        type = StreamType.SURFACE
+        format = StreamFormat.YUV_420_888
     )
 
-    private val streamConfig2 = StreamConfig(
+    private val streamConfig2 = CameraStream.Config.create(
         size = Size(640, 480),
-        format = StreamFormat.YUV_420_888,
-        camera = CameraId("test"),
-        type = StreamType.SURFACE
+        format = StreamFormat.YUV_420_888
     )
 
-    private val streamConfig3 = StreamConfig(
+    private val streamConfig3 = CameraStream.Config.create(
         size = Size(640, 480),
-        format = StreamFormat.JPEG,
-        camera = CameraId("test"),
-        type = StreamType.SURFACE
+        format = StreamFormat.JPEG
     )
 
     @Test
+    fun differentStreamConfigsAreNotEqual() {
+        assertThat(streamConfig1).isNotEqualTo(streamConfig3)
+        assertThat(streamConfig2).isNotEqualTo(streamConfig3)
+    }
+
+    @Test
     fun equivalentStreamConfigsAreNotEqual() {
         assertThat(streamConfig1).isNotEqualTo(streamConfig2)
         assertThat(streamConfig1).isNotSameInstanceAs(streamConfig2)
     }
 
     @Test
-    fun differentStreamConfigsAreNotEqual() {
-        assertThat(streamConfig1).isNotEqualTo(streamConfig3)
-        assertThat(streamConfig2).isNotEqualTo(streamConfig3)
+    fun equivalentOutputsAreNotEqual() {
+        assertThat(streamConfig1.outputs.single()).isNotEqualTo(streamConfig2.outputs.single())
+        assertThat(streamConfig1.outputs.single())
+            .isNotSameInstanceAs(streamConfig2.outputs.single())
+    }
+
+    @Test
+    fun sharedOutputsAreShared() {
+        val outputConfig = OutputStream.Config.create(
+            size = Size(640, 480),
+            format = StreamFormat.YUV_420_888
+        )
+        val sharedConfig1 = CameraStream.Config.create(outputConfig)
+        val sharedConfig2 = CameraStream.Config.create(outputConfig)
+        assertThat(sharedConfig1).isNotEqualTo(sharedConfig2)
+        assertThat(sharedConfig1.outputs.single()).isEqualTo(sharedConfig2.outputs.single())
+        assertThat(sharedConfig1.outputs.single()).isSameInstanceAs(sharedConfig2.outputs.single())
     }
 }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraGraphImplTest.kt
index e4d7239..c215ac1f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraGraphImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraGraphImplTest.kt
@@ -21,7 +21,6 @@
 import android.os.Build
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.Request
-import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
 import androidx.camera.camera2.pipe.testing.FakeCameras
@@ -53,13 +52,12 @@
         val config = CameraGraph.Config(
             camera = fakeCameraId,
             streams = listOf(),
-            template = RequestTemplate(0)
         )
         impl = CameraGraphImpl(
             config,
             fakeMetadata,
             fakeGraphProcessor,
-            StreamMap(
+            StreamGraphImpl(
                 fakeMetadata,
                 config
             ),
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraPipeComponentTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraPipeComponentTest.kt
index 0195662..ea02fe7 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraPipeComponentTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraPipeComponentTest.kt
@@ -20,7 +20,6 @@
 import android.os.Build
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
 import androidx.camera.camera2.pipe.testing.FakeCameras
 import androidx.test.core.app.ApplicationProvider
@@ -58,7 +57,6 @@
         val config = CameraGraph.Config(
             camera = cameraId,
             streams = listOf(),
-            template = RequestTemplate(0)
         )
         val module = CameraGraphConfigModule(config)
         val builder = component.cameraGraphComponentBuilder()
@@ -80,7 +78,6 @@
                     CameraGraph.Config(
                         camera = fakeCameraId,
                         streams = listOf(),
-                        template = RequestTemplate(0)
                     )
                 )
             )
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AForCaptureTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AForCaptureTest.kt
index c4b4465..61af5b8d 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AForCaptureTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AForCaptureTest.kt
@@ -109,10 +109,10 @@
         // We now check if the correct sequence of requests were submitted by lock3AForCapture call.
         // There should be a request to trigger AF and AE precapture metering.
         val request1 = requestProcessor.nextEvent().request
-        assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
-        assertThat(request1.extraRequestParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
+        assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
             .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
     }
 
@@ -186,10 +186,10 @@
         // We now check if the correct sequence of requests were submitted by unlock3APostCapture
         // call. There should be a request to cancel AF and AE precapture metering.
         val request1 = requestProcessor.nextEvent().request
-        assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
         )
-        assertThat(request1.extraRequestParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
+        assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
             .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL)
     }
 
@@ -223,15 +223,15 @@
         // We now check if the correct sequence of requests were submitted by unlock3APostCapture
         // call. There should be a request to cancel AF and lock ae.
         val request1 = requestProcessor.nextEvent().request
-        assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
         )
-        assertThat(request1.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK])
+        assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_LOCK])
             .isEqualTo(true)
 
         // Then another request to unlock ae.
         val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK])
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK])
             .isEqualTo(false)
     }
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt
index 016a709..4374aee 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt
@@ -115,16 +115,16 @@
         // We not check if the correct sequence of requests were submitted by lock3A call. The
         // request should be a repeating request to lock AE.
         val request1 = requestProcessor.nextEvent().request
-        assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request1!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
         // The second request should be a single request to lock AF.
         val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
-        assertThat(request2.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request2.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
     }
@@ -174,7 +174,7 @@
         requestProcessor.nextEvent().request
         // Once AE is converged, another repeatingrequest is sent to lock AE.
         val request1 = requestProcessor.nextEvent().request
-        assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request1!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
@@ -204,7 +204,7 @@
 
         // A single request to lock AF must have been used as well.
         val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
     }
@@ -250,7 +250,7 @@
         // For a new AE scan we first send a request to unlock AE just in case it was
         // previously or internally locked.
         val request1 = requestProcessor.nextEvent().request
-        assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request1!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             false
         )
 
@@ -280,16 +280,16 @@
 
         // There should be one more request to lock AE after new scan is done.
         val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
         // And one request to lock AF.
         val request3 = requestProcessor.nextEvent().request
-        assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        assertThat(request3!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
-        assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request3.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
     }
@@ -360,16 +360,16 @@
         requestProcessor.nextEvent()
         // One request to lock AE
         val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
         // And one request to lock AF.
         val request3 = requestProcessor.nextEvent().request
-        assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        assertThat(request3!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
-        assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request3.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
     }
@@ -438,7 +438,7 @@
 
         // One request to cancel AF to start a new scan.
         val request1 = requestProcessor.nextEvent().request
-        assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        assertThat(request1!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
         )
         // There should be one request to monitor AF to finish it's scan.
@@ -446,16 +446,16 @@
 
         // There should be one request to monitor lock AE.
         val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
         // And one request to lock AF.
         val request3 = requestProcessor.nextEvent().request
-        assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        assertThat(request3!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
-        assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request3.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
     }
@@ -526,16 +526,16 @@
         requestProcessor.nextEvent()
         // One request to lock AE
         val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
         // And one request to lock AF.
         val request3 = requestProcessor.nextEvent().request
-        assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        assertThat(request3!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
-        assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request3.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
     }
@@ -604,27 +604,27 @@
 
         // One request to cancel AF to start a new scan.
         val request1 = requestProcessor.nextEvent().request
-        assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        assertThat(request1!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
         )
         // There should be one request to unlock AE and monitor the current AF scan to finish.
         val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             false
         )
 
         // There should be one request to monitor lock AE.
         val request3 = requestProcessor.nextEvent().request
-        assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request3!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
         // And one request to lock AF.
         val request4 = requestProcessor.nextEvent().request
-        assertThat(request4!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        assertThat(request4!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
-        assertThat(request4.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request4.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
     }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASetTorchTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASetTorchTest.kt
new file mode 100644
index 0000000..18e7c2c
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASetTorchTest.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2020 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.camera.camera2.pipe.impl
+
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.os.Build
+import androidx.camera.camera2.pipe.AeMode
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.Status3A
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.TorchState
+import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
+import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
+import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
+import androidx.camera.camera2.pipe.testing.FakeRequestProcessor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(CameraPipeRobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+internal class Controller3ASetTorchTest {
+    private val graphProcessor = FakeGraphProcessor()
+    private val graphState3A = GraphState3A()
+    private val requestProcessor = FakeRequestProcessor(graphState3A)
+    private val listener3A = Listener3A()
+    private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
+
+    @Test
+    fun testSetTorchOn() = runBlocking {
+        initGraphProcessor()
+
+        val result = controller3A.setTorch(TorchState.ON)
+        assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON)
+        assertThat(graphState3A.flashMode!!.value).isEqualTo(CaptureRequest.FLASH_MODE_TORCH)
+        assertThat(result.isCompleted).isFalse()
+
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
+                        CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH
+                    )
+                )
+            )
+        }
+        val result3A = result.await()
+        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.status).isEqualTo(Status3A.OK)
+    }
+
+    @Test
+    fun testSetTorchOff() = runBlocking {
+        initGraphProcessor()
+
+        val result = controller3A.setTorch(TorchState.OFF)
+        assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON)
+        assertThat(graphState3A.flashMode!!.value).isEqualTo(CaptureRequest.FLASH_MODE_OFF)
+        assertThat(result.isCompleted).isFalse()
+
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
+                        CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_OFF
+                    )
+                )
+            )
+        }
+        val result3A = result.await()
+        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.status).isEqualTo(Status3A.OK)
+    }
+
+    @Test
+    fun testSetTorchDoesNotChangeAeModeIfNotNeeded() = runBlocking {
+        initGraphProcessor()
+
+        graphState3A.update(aeMode = AeMode.OFF)
+
+        val result = controller3A.setTorch(TorchState.ON)
+        assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_OFF)
+        assertThat(graphState3A.flashMode!!.value).isEqualTo(
+            CaptureRequest.FLASH_MODE_TORCH
+        )
+        assertThat(result.isCompleted).isFalse()
+
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_OFF,
+                        CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH
+                    )
+                )
+            )
+        }
+        val result3A = result.await()
+        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.status).isEqualTo(Status3A.OK)
+    }
+
+    private fun initGraphProcessor() {
+        graphProcessor.attach(requestProcessor)
+        graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
index 3c4f330..6309f96 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
@@ -56,9 +56,7 @@
         initGraphProcessor()
 
         val result = controller3A.submit3A(afMode = AfMode.OFF)
-        assertThat(graphState3A.readState()[CaptureRequest.CONTROL_AF_MODE]).isNotEqualTo(
-            CaptureRequest.CONTROL_AE_MODE_OFF
-        )
+        assertThat(graphState3A.afMode?.value).isNotEqualTo(CaptureRequest.CONTROL_AF_MODE_OFF)
         assertThat(result.isCompleted).isFalse()
     }
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt
index 53052d7..ed9455b 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt
@@ -85,7 +85,7 @@
 
         // There should be one request to lock AE.
         val request1 = requestProcessor.nextEvent().request
-        Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK])
+        Truth.assertThat(request1!!.parameters[CaptureRequest.CONTROL_AE_LOCK])
             .isEqualTo(false)
 
         GlobalScope.launch {
@@ -146,7 +146,7 @@
 
         // There should be one request to unlock AF.
         val request1 = requestProcessor.nextEvent().request
-        Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+        Truth.assertThat(request1!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER])
             .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
 
         GlobalScope.launch {
@@ -209,7 +209,7 @@
 
         // There should be one request to lock AWB.
         val request1 = requestProcessor.nextEvent().request
-        Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AWB_LOCK])
+        Truth.assertThat(request1!!.parameters[CaptureRequest.CONTROL_AWB_LOCK])
             .isEqualTo(false)
 
         GlobalScope.launch {
@@ -271,11 +271,11 @@
 
         // There should be one request to unlock AF.
         val request1 = requestProcessor.nextEvent().request
-        Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+        Truth.assertThat(request1!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER])
             .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
         // Then request to unlock AE.
         val request2 = requestProcessor.nextEvent().request
-        Truth.assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK])
+        Truth.assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK])
             .isEqualTo(false)
 
         GlobalScope.launch {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
index 30a21b5..365e6fb 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
@@ -57,9 +57,7 @@
         initGraphProcessor()
 
         val result = controller3A.update3A(afMode = AfMode.OFF)
-        assertThat(graphState3A.readState()[CaptureRequest.CONTROL_AF_MODE]).isEqualTo(
-            CaptureRequest.CONTROL_AE_MODE_OFF
-        )
+        assertThat(graphState3A.afMode!!.value).isEqualTo(CaptureRequest.CONTROL_AF_MODE_OFF)
         assertThat(result.isCompleted).isFalse()
     }
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
index 6c3c06a..a3e5555 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
@@ -17,6 +17,8 @@
 package androidx.camera.camera2.pipe.impl
 
 import android.os.Build
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
@@ -45,6 +47,11 @@
     private val requestListener2 = FakeRequestListener()
     private val request2 = Request(listOf(StreamId(0)), listeners = listOf(requestListener2))
 
+    private val graphConfig = CameraGraph.Config(
+        camera = CameraId.fromCamera2Id("CameraId-Test"),
+        streams = listOf()
+    )
+
     @Test
     fun graphProcessorSubmitsRequests() {
         // The graph processor uses 'launch' within the coroutine scope to invoke updates on the
@@ -53,6 +60,7 @@
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
+                graphConfig,
                 this,
                 arrayListOf(globalListener)
             )
@@ -74,6 +82,7 @@
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
+                graphConfig,
                 this,
                 arrayListOf(globalListener)
             )
@@ -100,6 +109,7 @@
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
+                graphConfig,
                 this,
                 arrayListOf(globalListener)
             )
@@ -126,6 +136,7 @@
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
+                graphConfig,
                 this,
                 arrayListOf(globalListener)
             )
@@ -144,6 +155,7 @@
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
+                graphConfig,
                 this,
                 arrayListOf(globalListener)
             )
@@ -178,6 +190,7 @@
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
+                graphConfig,
                 this,
                 arrayListOf(globalListener)
             )
@@ -220,6 +233,7 @@
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
+                graphConfig,
                 this,
                 arrayListOf(globalListener)
             )
@@ -237,6 +251,7 @@
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
+                graphConfig,
                 this,
                 arrayListOf(globalListener)
             )
@@ -258,6 +273,7 @@
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
+                graphConfig,
                 this,
                 arrayListOf(globalListener)
             )
@@ -278,6 +294,7 @@
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
+                graphConfig,
                 this,
                 arrayListOf(globalListener)
             )
@@ -297,6 +314,7 @@
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
+                graphConfig,
                 this,
                 arrayListOf(globalListener)
             )
@@ -322,6 +340,7 @@
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
+                graphConfig,
                 this,
                 arrayListOf(globalListener)
             )
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
index 26fcbcf..40fa0a3 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
@@ -49,7 +49,7 @@
 internal interface CameraSessionTestComponent {
     fun graphConfig(): CameraGraph.Config
     fun sessionFactory(): SessionFactory
-    fun streamMap(): StreamMap
+    fun streamMap(): StreamGraphImpl
 }
 
 @RunWith(CameraPipeRobolectricTestRunner::class)
@@ -89,13 +89,14 @@
 
         val sessionFactory = component.sessionFactory()
         val streamMap = component.streamMap()
-        val streamConfig = component.graphConfig().streams.first()
-        val stream1 = streamMap.streamConfigMap[streamConfig]!!
+        val cameraStreamConfig = component.graphConfig().streams.first()
+        val stream1 = streamMap[cameraStreamConfig]!!
+        val stream1Output = stream1.outputs.first()
 
         val surfaceTexture = SurfaceTexture(0)
         surfaceTexture.setDefaultBufferSize(
-            stream1.size.width,
-            stream1.size.height
+            stream1Output.size.width,
+            stream1Output.size.height
         )
         val surface = Surface(surfaceTexture)
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamGraphImplTest.kt
new file mode 100644
index 0000000..8391d35
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamGraphImplTest.kt
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2020 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.camera.camera2.pipe.impl
+
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+import android.os.Build
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraStream
+import androidx.camera.camera2.pipe.OutputStream
+import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(CameraPipeRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+internal class StreamGraphImplTest {
+    private val fakeMetadata = FakeCameraMetadata(
+        mapOf(INFO_SUPPORTED_HARDWARE_LEVEL to INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
+    )
+
+    private val camera1 = CameraId("TestCamera-1")
+    private val camera2 = CameraId("TestCamera-2")
+
+    private val streamConfig1 = CameraStream.Config.create(
+        size = Size(100, 100),
+        format = StreamFormat.YUV_420_888
+    )
+    private val streamConfig2 = CameraStream.Config.create(
+        size = Size(123, 321),
+        format = StreamFormat.YUV_420_888,
+        camera = camera1
+    )
+    private val streamConfig3 = CameraStream.Config.create(
+        size = Size(200, 200),
+        format = StreamFormat.YUV_420_888,
+        camera = camera2,
+        outputType = OutputStream.OutputType.SURFACE_TEXTURE
+    )
+    private val sharedOutputConfig = OutputStream.Config.create(
+        size = Size(200, 200),
+        format = StreamFormat.YUV_420_888,
+        camera = camera1
+    )
+    private val sharedStreamConfig1 = CameraStream.Config.create(sharedOutputConfig)
+    private val sharedStreamConfig2 = CameraStream.Config.create(sharedOutputConfig)
+
+    private val graphConfig = CameraGraph.Config(
+        camera = camera1,
+        streams = listOf(
+            streamConfig1,
+            streamConfig2,
+            streamConfig3,
+            sharedStreamConfig1,
+            sharedStreamConfig2
+        ),
+        streamSharingGroups = listOf(listOf(streamConfig1, streamConfig2))
+    )
+
+    @Test
+    fun testPrecomputedTestData() {
+        val streamGraph = StreamGraphImpl(fakeMetadata, graphConfig)
+
+        assertThat(streamGraph.streams).hasSize(5)
+        assertThat(streamGraph.streams).hasSize(5)
+        assertThat(streamGraph.outputConfigs).hasSize(4)
+
+        val stream1 = streamGraph[streamConfig1]!!
+        val outputStream1 = stream1.outputs.single()
+        assertThat(outputStream1.format).isEqualTo(StreamFormat.YUV_420_888)
+        assertThat(outputStream1.size.width).isEqualTo(100)
+        assertThat(outputStream1.size.height).isEqualTo(100)
+
+        val stream2 = streamGraph[streamConfig2]!!
+        val outputStream2 = stream2.outputs.single()
+        assertThat(outputStream2.camera).isEqualTo(graphConfig.camera)
+        assertThat(outputStream2.format).isEqualTo(StreamFormat.YUV_420_888)
+        assertThat(outputStream2.size.width).isEqualTo(123)
+        assertThat(outputStream2.size.height).isEqualTo(321)
+    }
+
+    @Test
+    fun testStreamGraphPopulatesCameraId() {
+        val streamGraph = StreamGraphImpl(fakeMetadata, graphConfig)
+        val stream = streamGraph[streamConfig1]!!
+        assertThat(streamConfig1.outputs.single().camera).isNull()
+        assertThat(stream.outputs.single().camera).isEqualTo(graphConfig.camera)
+    }
+
+    @Test
+    fun testStreamWithMultipleOutputs() {
+
+        val streamConfig = CameraStream.Config.create(
+            listOf(
+                OutputStream.Config.create(
+                    Size(800, 600),
+                    StreamFormat.YUV_420_888
+                ),
+                OutputStream.Config.create(
+                    Size(1600, 1200),
+                    StreamFormat.YUV_420_888
+                ),
+                OutputStream.Config.create(
+                    Size(800, 600),
+                    StreamFormat.YUV_420_888
+                ),
+            )
+        )
+        val config = CameraGraph.Config(
+            camera = CameraId("TestCamera"),
+            streams = listOf(streamConfig),
+        )
+        val streamGraph = StreamGraphImpl(fakeMetadata, config)
+
+        assertThat(streamGraph.streams).hasSize(1)
+        assertThat(streamGraph.streams).hasSize(1)
+        assertThat(streamGraph.outputConfigs).hasSize(3)
+    }
+
+    @Test
+    fun testStreamMapConvertsConfigObjectsToStreamIds() {
+        val streamGraph = StreamGraphImpl(fakeMetadata, graphConfig)
+
+        assertThat(streamGraph[streamConfig1]).isNotNull()
+        assertThat(streamGraph[streamConfig2]).isNotNull()
+        assertThat(streamGraph[streamConfig3]).isNotNull()
+
+        val stream1 = streamGraph[streamConfig1]!!
+        val stream2 = streamGraph[streamConfig2]!!
+        val stream3 = streamGraph[streamConfig3]!!
+
+        assertThat(stream1).isEqualTo(streamGraph[streamConfig1])
+        assertThat(stream2).isEqualTo(streamGraph[streamConfig2])
+        assertThat(stream3).isEqualTo(streamGraph[streamConfig3])
+
+        assertThat(streamConfig1).isNotEqualTo(streamConfig2)
+        assertThat(streamConfig1).isNotEqualTo(streamConfig3)
+        assertThat(streamConfig2).isNotEqualTo(streamConfig3)
+    }
+
+    @Test
+    fun testStreamMapIdsAreNotEqualAcrossMultipleStreamMapInstances() {
+        val streamGraphA = StreamGraphImpl(fakeMetadata, graphConfig)
+        val streamGraphB = StreamGraphImpl(fakeMetadata, graphConfig)
+
+        val stream1A = streamGraphA[streamConfig1]!!
+        val stream1B = streamGraphB[streamConfig1]!!
+
+        assertThat(stream1A).isNotEqualTo(stream1B)
+        assertThat(stream1A.id).isNotEqualTo(stream1B.id)
+    }
+
+    @Test
+    fun testSharedStreamsHaveOneOutputConfig() {
+        val streamGraph = StreamGraphImpl(fakeMetadata, graphConfig)
+        val stream1 = streamGraph[sharedStreamConfig1]!!
+        val stream2 = streamGraph[sharedStreamConfig2]!!
+
+        val outputConfigForStream1 =
+            streamGraph.outputConfigs.filter { it.streams.contains(stream1) }
+        val outputConfigForStream2 =
+            streamGraph.outputConfigs.filter { it.streams.contains(stream2) }
+
+        assertThat(outputConfigForStream1).hasSize(1)
+        assertThat(outputConfigForStream2).hasSize(1)
+        assertThat(outputConfigForStream1.first()).isSameInstanceAs(outputConfigForStream2.first())
+    }
+
+    @Test
+    fun testSharedStreamsHaveDifferentOutputStreams() {
+        val streamGraph = StreamGraphImpl(fakeMetadata, graphConfig)
+        val stream1 = streamGraph[sharedStreamConfig1]!!
+        val stream2 = streamGraph[sharedStreamConfig2]!!
+
+        assertThat(stream1.outputs.first()).isNotEqualTo(stream2.outputs.first())
+    }
+
+    @Test
+    fun testGroupedStreamsHaveSameGroupNumber() {
+        val streamGraph = StreamGraphImpl(fakeMetadata, graphConfig)
+        val stream1 = streamGraph[streamConfig1]!!
+        val stream2 = streamGraph[streamConfig2]!!
+
+        val outputConfigForStream1 =
+            streamGraph.outputConfigs.filter { it.streams.contains(stream1) }
+        val outputConfigForStream2 =
+            streamGraph.outputConfigs.filter { it.streams.contains(stream2) }
+        assertThat(outputConfigForStream1).hasSize(1)
+        assertThat(outputConfigForStream2).hasSize(1)
+
+        val config1 = outputConfigForStream1.first()
+        val config2 = outputConfigForStream2.first()
+        assertThat(config1).isNotEqualTo(config2)
+
+        assertThat(config1.groupNumber).isGreaterThan(-1)
+        assertThat(config2.groupNumber).isGreaterThan(-1)
+        assertThat(config1.groupNumber).isEqualTo(config2.groupNumber)
+    }
+
+    @Test
+    fun outputSurfacesArePassedToListenerImmediately() {
+        val streamMap = StreamGraphImpl(fakeMetadata, graphConfig)
+        val stream1 = streamMap[streamConfig1]!!
+        val stream2 = streamMap[streamConfig2]!!
+        val stream3 = streamMap[streamConfig3]!!
+        val stream4 = streamMap[sharedStreamConfig1]!!
+        val stream5 = streamMap[sharedStreamConfig2]!!
+
+        val fakeSurface1 = Surface(SurfaceTexture(1))
+        val fakeSurface2 = Surface(SurfaceTexture(2))
+        val fakeSurface3 = Surface(SurfaceTexture(3))
+        val fakeSurface4 = Surface(SurfaceTexture(4))
+        val fakeSurface5 = Surface(SurfaceTexture(5))
+
+        streamMap[stream1.id] = fakeSurface1
+        streamMap[stream2.id] = fakeSurface2
+        streamMap[stream3.id] = fakeSurface3
+        streamMap[stream4.id] = fakeSurface4
+        streamMap[stream5.id] = fakeSurface5
+
+        val session = FakeSurfaceListener()
+
+        streamMap.listener = session
+
+        assertThat(session.surfaces).isNotNull()
+        assertThat(session.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1)
+        assertThat(session.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
+        assertThat(session.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
+    }
+
+    @Test
+    fun outputSurfacesArePassedToListenerWhenAvailable() {
+        val streamMap = StreamGraphImpl(fakeMetadata, graphConfig)
+        val stream1 = streamMap[streamConfig1]!!
+        val stream2 = streamMap[streamConfig2]!!
+        val stream3 = streamMap[streamConfig3]!!
+        val stream4 = streamMap[sharedStreamConfig1]!!
+        val stream5 = streamMap[sharedStreamConfig2]!!
+
+        val fakeSurface1 = Surface(SurfaceTexture(1))
+        val fakeSurface2 = Surface(SurfaceTexture(2))
+        val fakeSurface3 = Surface(SurfaceTexture(3))
+        val fakeSurface4 = Surface(SurfaceTexture(4))
+        val fakeSurface5 = Surface(SurfaceTexture(5))
+
+        val session = FakeSurfaceListener()
+        streamMap.listener = session
+        assertThat(session.surfaces).isNull()
+
+        streamMap[stream1.id] = fakeSurface1
+        streamMap[stream2.id] = fakeSurface2
+        streamMap[stream3.id] = fakeSurface3
+        assertThat(session.surfaces).isNull()
+
+        streamMap[stream4.id] = fakeSurface4
+        streamMap[stream5.id] = fakeSurface5
+
+        assertThat(session.surfaces).isNotNull()
+        assertThat(session.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1)
+        assertThat(session.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
+        assertThat(session.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
+        assertThat(session.surfaces?.get(stream4.id)).isEqualTo(fakeSurface4)
+        assertThat(session.surfaces?.get(stream5.id)).isEqualTo(fakeSurface5)
+    }
+
+    @Test
+    fun onlyFinalSurfacesAreSentToSession() {
+        val streamMap = StreamGraphImpl(fakeMetadata, graphConfig)
+        val stream1 = streamMap[streamConfig1]!!
+        val stream2 = streamMap[streamConfig2]!!
+        val stream3 = streamMap[streamConfig3]!!
+        val stream4 = streamMap[sharedStreamConfig1]!!
+        val stream5 = streamMap[sharedStreamConfig2]!!
+
+        val fakeSurface1A = Surface(SurfaceTexture(1))
+        val fakeSurface1B = Surface(SurfaceTexture(2))
+        val fakeSurface2 = Surface(SurfaceTexture(3))
+        val fakeSurface3 = Surface(SurfaceTexture(4))
+        val fakeSurface4 = Surface(SurfaceTexture(5))
+        val fakeSurface5 = Surface(SurfaceTexture(6))
+
+        val session = FakeSurfaceListener()
+        streamMap.listener = session
+        streamMap[stream1.id] = fakeSurface1A
+        streamMap[stream1.id] = fakeSurface1B
+        assertThat(session.surfaces).isNull()
+
+        streamMap[stream2.id] = fakeSurface2
+        streamMap[stream3.id] = fakeSurface3
+        streamMap[stream4.id] = fakeSurface4
+        streamMap[stream5.id] = fakeSurface5
+
+        assertThat(session.surfaces).isNotNull()
+        assertThat(session.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1B)
+        assertThat(session.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
+        assertThat(session.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
+        assertThat(session.surfaces?.get(stream4.id)).isEqualTo(fakeSurface4)
+        assertThat(session.surfaces?.get(stream5.id)).isEqualTo(fakeSurface5)
+    }
+
+    @Test
+    fun settingListenerToNullDoesNotClearSurfaces() {
+        val streamMap = StreamGraphImpl(fakeMetadata, graphConfig)
+        val stream1 = streamMap[streamConfig1]!!
+        val stream2 = streamMap[streamConfig2]!!
+        val stream3 = streamMap[streamConfig3]!!
+
+        val fakeSurface1 = Surface(SurfaceTexture(1))
+        val fakeSurface2 = Surface(SurfaceTexture(2))
+        val fakeSurface3 = Surface(SurfaceTexture(3))
+
+        val session = FakeSurfaceListener()
+        streamMap.listener = session
+        streamMap[stream1.id] = fakeSurface1
+        streamMap.listener = null
+
+        streamMap[stream2.id] = fakeSurface2
+        streamMap[stream3.id] = fakeSurface3
+
+        assertThat(session.surfaces).isNull()
+    }
+
+    @Test
+    fun replacingSessionPassesSurfacesToNewSession() {
+        val streamMap = StreamGraphImpl(fakeMetadata, graphConfig)
+        val stream1 = streamMap[streamConfig1]!!
+        val stream2 = streamMap[streamConfig2]!!
+        val stream3 = streamMap[streamConfig3]!!
+        val stream4 = streamMap[sharedStreamConfig1]!!
+        val stream5 = streamMap[sharedStreamConfig2]!!
+
+        val fakeSurface1 = Surface(SurfaceTexture(1))
+        val fakeSurface2 = Surface(SurfaceTexture(2))
+        val fakeSurface3 = Surface(SurfaceTexture(3))
+        val fakeSurface4 = Surface(SurfaceTexture(4))
+        val fakeSurface5 = Surface(SurfaceTexture(5))
+
+        streamMap[stream1.id] = fakeSurface1
+        streamMap[stream2.id] = fakeSurface2
+        streamMap[stream3.id] = fakeSurface3
+        streamMap[stream4.id] = fakeSurface4
+        streamMap[stream5.id] = fakeSurface5
+
+        val listener1 = FakeSurfaceListener()
+        streamMap.listener = listener1
+
+        val listener2 = FakeSurfaceListener()
+        streamMap.listener = listener2
+
+        assertThat(listener2.surfaces).isNotNull()
+        assertThat(listener2.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1)
+        assertThat(listener2.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
+        assertThat(listener2.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
+        assertThat(listener2.surfaces?.get(stream4.id)).isEqualTo(fakeSurface4)
+        assertThat(listener2.surfaces?.get(stream5.id)).isEqualTo(fakeSurface5)
+    }
+
+    class FakeSurfaceListener : StreamGraphImpl.SurfaceListener {
+        var surfaces: Map<StreamId, Surface>? = null
+
+        override fun onSurfaceMapUpdated(surfaces: Map<StreamId, Surface>) {
+            this.surfaces = surfaces
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamMapTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamMapTest.kt
deleted file mode 100644
index 68fa61b..0000000
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamMapTest.kt
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * Copyright 2020 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.camera.camera2.pipe.impl
-
-import android.graphics.SurfaceTexture
-import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
-import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
-import android.os.Build
-import android.util.Size
-import android.view.Surface
-import androidx.camera.camera2.pipe.CameraGraph
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.RequestTemplate
-import androidx.camera.camera2.pipe.StreamConfig
-import androidx.camera.camera2.pipe.StreamFormat
-import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.StreamType
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
-import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.internal.DoNotInstrument
-
-@RunWith(CameraPipeRobolectricTestRunner::class)
-@DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-internal class StreamMapTest {
-    private val fakeMetadata = FakeCameraMetadata(
-        mapOf(INFO_SUPPORTED_HARDWARE_LEVEL to INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
-    )
-
-    private val camera1 = CameraId("TestCamera-1")
-    private val camera2 = CameraId("TestCamera-2")
-
-    private val streamConfig1 = StreamConfig(
-        size = Size(100, 100),
-        format = StreamFormat.YUV_420_888,
-        camera = camera1,
-        type = StreamType.SURFACE,
-        deferrable = false
-    )
-
-    private val streamConfig2 = StreamConfig(
-        size = Size(100, 100),
-        format = StreamFormat.YUV_420_888,
-        camera = camera1,
-        type = StreamType.SURFACE,
-        deferrable = false
-    )
-
-    private val streamConfig3 =
-        StreamConfig(
-            size = Size(200, 200),
-            format = StreamFormat.YUV_420_888,
-            camera = camera2,
-            type = StreamType.SURFACE_TEXTURE,
-            deferrable = true
-        )
-
-    private val graphConfig = CameraGraph.Config(
-        camera = CameraId("0"),
-        streams = listOf(
-            streamConfig1,
-            streamConfig2,
-            streamConfig3
-        ),
-        template = RequestTemplate(0)
-    )
-
-    @Test
-    fun testStreamMapConvertsConfigObjectsToStreamIds() {
-        val streamMap = StreamMap(fakeMetadata, graphConfig)
-
-        assertThat(streamMap.streamConfigMap[streamConfig1]).isNotNull()
-        assertThat(streamMap.streamConfigMap[streamConfig2]).isNotNull()
-        assertThat(streamMap.streamConfigMap[streamConfig3]).isNotNull()
-
-        val stream1 = streamMap.streamConfigMap[streamConfig1]!!
-        val stream2 = streamMap.streamConfigMap[streamConfig2]!!
-        val stream3 = streamMap.streamConfigMap[streamConfig3]!!
-
-        assertThat(stream1).isEqualTo(streamMap.streamConfigMap[streamConfig1])
-        assertThat(stream2).isEqualTo(streamMap.streamConfigMap[streamConfig2])
-        assertThat(stream3).isEqualTo(streamMap.streamConfigMap[streamConfig3])
-
-        assertThat(stream1).isNotEqualTo(stream2)
-        assertThat(stream1).isNotEqualTo(stream3)
-        assertThat(stream2).isNotEqualTo(stream3)
-    }
-
-    @Test
-    fun testStreamMapIdsAreNotEqualAcrossMultipleStreamMapInstances() {
-        val streamMap1 = StreamMap(fakeMetadata, graphConfig)
-        val streamMap2 = StreamMap(fakeMetadata, graphConfig)
-
-        val stream1FromConfig1 = streamMap1.streamConfigMap[streamConfig1]
-        val stream1FromConfig2 = streamMap2.streamConfigMap[streamConfig1]
-
-        assertThat(stream1FromConfig1).isNotEqualTo(stream1FromConfig2)
-    }
-
-    @Test
-    fun streamsFromSameConfigAreDifferent() {
-        val stream1 = StreamMap.StreamImpl(
-            StreamId(1),
-            streamConfig1.size,
-            streamConfig1.format,
-            streamConfig1.camera,
-            streamConfig1.type
-        )
-        val stream2 = StreamMap.StreamImpl(
-            StreamId(2),
-            streamConfig1.size,
-            streamConfig1.format,
-            streamConfig1.camera,
-            streamConfig1.type
-        )
-
-        assertThat(stream1).isNotEqualTo(stream2)
-    }
-
-    @Test
-    fun surfacesAreSetOnVirtualCaptureSession() {
-        val streamMap = StreamMap(fakeMetadata, graphConfig)
-        val stream1 = streamMap.streamConfigMap[streamConfig1]!!
-        val stream2 = streamMap.streamConfigMap[streamConfig2]!!
-        val stream3 = streamMap.streamConfigMap[streamConfig3]!!
-
-        val fakeSurface1 = Surface(SurfaceTexture(1))
-        val fakeSurface2 = Surface(SurfaceTexture(2))
-        val fakeSurface3 = Surface(SurfaceTexture(3))
-
-        streamMap[stream1.id] = fakeSurface1
-        streamMap[stream2.id] = fakeSurface2
-        streamMap[stream3.id] = fakeSurface3
-
-        val session = FakeSurfaceListener()
-
-        streamMap.listener = session
-
-        assertThat(session.surfaces).isNotNull()
-        assertThat(session.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1)
-        assertThat(session.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
-        assertThat(session.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
-    }
-
-    @Test
-    fun surfacesAreSetOnceAllSurfacesAreAvailable() {
-        val streamMap = StreamMap(fakeMetadata, graphConfig)
-        val stream1 = streamMap.streamConfigMap[streamConfig1]!!
-        val stream2 = streamMap.streamConfigMap[streamConfig2]!!
-        val stream3 = streamMap.streamConfigMap[streamConfig3]!!
-
-        val fakeSurface1 = Surface(SurfaceTexture(1))
-        val fakeSurface2 = Surface(SurfaceTexture(2))
-        val fakeSurface3 = Surface(SurfaceTexture(3))
-
-        val session = FakeSurfaceListener()
-        streamMap.listener = session
-        assertThat(session.surfaces).isNull()
-
-        streamMap[stream1.id] = fakeSurface1
-        assertThat(session.surfaces).isNull()
-
-        streamMap[stream2.id] = fakeSurface2
-        streamMap[stream3.id] = fakeSurface3
-
-        assertThat(session.surfaces).isNotNull()
-        assertThat(session.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1)
-        assertThat(session.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
-        assertThat(session.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
-    }
-
-    @Test
-    fun onlyFinalSurfacesAreSentToSession() {
-        val streamMap = StreamMap(fakeMetadata, graphConfig)
-        val stream1 = streamMap.streamConfigMap[streamConfig1]!!
-        val stream2 = streamMap.streamConfigMap[streamConfig2]!!
-        val stream3 = streamMap.streamConfigMap[streamConfig3]!!
-
-        val fakeSurface1 = Surface(SurfaceTexture(1))
-        val fakeSurface2 = Surface(SurfaceTexture(2))
-        val fakeSurface3 = Surface(SurfaceTexture(3))
-        val fakeSurface4 = Surface(SurfaceTexture(4))
-
-        val session = FakeSurfaceListener()
-        streamMap.listener = session
-        streamMap[stream1.id] = fakeSurface1
-        streamMap[stream1.id] = fakeSurface2
-        assertThat(session.surfaces).isNull()
-
-        streamMap[stream2.id] = fakeSurface3
-        streamMap[stream3.id] = fakeSurface4
-
-        assertThat(session.surfaces).isNotNull()
-        assertThat(session.surfaces?.get(stream1.id)).isEqualTo(fakeSurface2)
-        assertThat(session.surfaces?.get(stream2.id)).isEqualTo(fakeSurface3)
-        assertThat(session.surfaces?.get(stream3.id)).isEqualTo(fakeSurface4)
-    }
-
-    @Test
-    fun settingSessionToNullDoesNotSetSurfaces() {
-        val streamMap = StreamMap(fakeMetadata, graphConfig)
-        val stream1 = streamMap.streamConfigMap[streamConfig1]!!
-        val stream2 = streamMap.streamConfigMap[streamConfig2]!!
-        val stream3 = streamMap.streamConfigMap[streamConfig3]!!
-
-        val fakeSurface1 = Surface(SurfaceTexture(1))
-        val fakeSurface2 = Surface(SurfaceTexture(2))
-        val fakeSurface3 = Surface(SurfaceTexture(3))
-
-        val session = FakeSurfaceListener()
-        streamMap.listener = session
-        streamMap[stream1.id] = fakeSurface1
-        streamMap.listener = null
-
-        streamMap[stream2.id] = fakeSurface2
-        streamMap[stream3.id] = fakeSurface3
-
-        assertThat(session.surfaces).isNull()
-    }
-
-    @Test
-    fun replacingSessionPassesSurfacesToNewSession() {
-        val streamMap = StreamMap(fakeMetadata, graphConfig)
-        val stream1 = streamMap.streamConfigMap[streamConfig1]!!
-        val stream2 = streamMap.streamConfigMap[streamConfig2]!!
-        val stream3 = streamMap.streamConfigMap[streamConfig3]!!
-
-        val fakeSurface1 = Surface(SurfaceTexture(1))
-        val fakeSurface2 = Surface(SurfaceTexture(2))
-        val fakeSurface3 = Surface(SurfaceTexture(3))
-
-        streamMap[stream1.id] = fakeSurface1
-        streamMap[stream2.id] = fakeSurface2
-        streamMap[stream3.id] = fakeSurface3
-
-        val session1 = FakeSurfaceListener()
-        streamMap.listener = session1
-
-        val session2 = FakeSurfaceListener()
-        streamMap.listener = session2
-
-        assertThat(session2.surfaces).isNotNull()
-        assertThat(session2.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1)
-        assertThat(session2.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
-        assertThat(session2.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
-    }
-
-    class FakeSurfaceListener : SurfaceListener {
-        var surfaces: Map<StreamId, Surface>? = null
-
-        override fun setSurfaceMap(surfaces: Map<StreamId, Surface>) {
-            this.surfaces = surfaces
-        }
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt
index 0842c90..60f9c9b 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt
@@ -18,6 +18,7 @@
 
 import android.os.Build
 import android.os.Looper.getMainLooper
+import androidx.camera.camera2.pipe.core.Timestamps
 import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
 import androidx.camera.camera2.pipe.testing.FakeCameras
 import com.google.common.truth.Truth.assertThat
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt
index 9260ebc..c11a475 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt
@@ -31,10 +31,8 @@
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.RequestTemplate
-import androidx.camera.camera2.pipe.StreamConfig
+import androidx.camera.camera2.pipe.CameraStream.Config
 import androidx.camera.camera2.pipe.StreamFormat
-import androidx.camera.camera2.pipe.StreamType
 import androidx.camera.camera2.pipe.impl.CameraGraphModules
 import androidx.camera.camera2.pipe.impl.CameraMetadataImpl
 import androidx.camera.camera2.pipe.impl.CameraPipeModules
@@ -195,18 +193,16 @@
 
         @Provides
         @Singleton
-        fun provideFakeGraphConfig() = CameraGraph.Config(
-            camera = fakeCamera.cameraId,
-            streams = listOf(
-                StreamConfig(
-                    Size(640, 480),
-                    StreamFormat.YUV_420_888,
-                    fakeCamera.cameraId,
-                    StreamType.SURFACE
-                )
-            ),
-            template = RequestTemplate(0)
-        )
+        fun provideFakeGraphConfig(): CameraGraph.Config {
+            val stream = Config.create(
+                Size(640, 480),
+                StreamFormat.YUV_420_888
+            )
+            return CameraGraph.Config(
+                camera = fakeCamera.cameraId,
+                streams = listOf(stream),
+            )
+        }
     }
 
     @Module(includes = [CameraGraphModules::class])
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
index 6757829..dde3ba50 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
@@ -16,7 +16,6 @@
 
 package androidx.camera.camera2.pipe.testing
 
-import android.hardware.camera2.CaptureRequest
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.impl.GraphProcessor
 import androidx.camera.camera2.pipe.impl.RequestProcessor
@@ -36,6 +35,7 @@
 
     private val _requestQueue = mutableListOf<List<Request>>()
     private var processor: RequestProcessor? = null
+    private val defaultParameters = mapOf<Any, Any>()
 
     override fun setRepeating(request: Request) {
         repeatingRequest = request
@@ -49,7 +49,7 @@
         _requestQueue.add(requests)
     }
 
-    override suspend fun submit(parameters: Map<CaptureRequest.Key<*>, Any>): Boolean {
+    override suspend fun submit(parameters: Map<*, Any>): Boolean {
         if (closed) {
             return false
         }
@@ -59,8 +59,8 @@
             currProcessor == null || currRepeatingRequest == null -> false
             else -> currProcessor.submit(
                 currRepeatingRequest,
-                parameters,
-                requireSurfacesForAllStreams = false
+                defaultParameters = defaultParameters,
+                requiredParameters = parameters
             )
         }
     }
@@ -93,6 +93,6 @@
     }
 
     override fun invalidate() {
-        processor!!.setRepeating(repeatingRequest!!, mapOf(), false)
+        processor!!.setRepeating(repeatingRequest!!, defaultParameters, mapOf<Any, Any>())
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
index 35adddc..006a544 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
@@ -16,11 +16,11 @@
 
 package androidx.camera.camera2.pipe.testing
 
-import android.hardware.camera2.CaptureRequest
 import android.view.Surface
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.impl.GraphState3A
+import androidx.camera.camera2.pipe.impl.RequestProcessorFactory
 import androidx.camera.camera2.pipe.impl.RequestProcessor
 import androidx.camera.camera2.pipe.impl.TokenLock
 import androidx.camera.camera2.pipe.impl.TokenLockImpl
@@ -32,7 +32,7 @@
  * Fake implementation of a [RequestProcessor] for tests.
  */
 internal class FakeRequestProcessor(private val graphState3A: GraphState3A) :
-    RequestProcessor, RequestProcessor.Factory {
+    RequestProcessor, RequestProcessorFactory {
     private val eventChannel = Channel<Event>(Channel.UNLIMITED)
 
     val requestQueue: MutableList<FakeRequest> = mutableListOf()
@@ -54,7 +54,10 @@
 
     data class FakeRequest(
         val burst: List<Request>,
-        val extraRequestParameters: Map<CaptureRequest.Key<*>, Any> = emptyMap(),
+        val defaultParameters: Map<*, Any>,
+        val internalParameters: Map<*, Any>,
+        val requiredParameters: Map<*, Any>,
+        val parameters: Map<*, Any>,
         val requireStreams: Boolean = false
     )
 
@@ -69,11 +72,11 @@
 
     override fun submit(
         request: Request,
-        extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
-        requireSurfacesForAllStreams: Boolean
+        defaultParameters: Map<*, Any>,
+        requiredParameters: Map<*, Any>
     ): Boolean {
         val fakeRequest =
-            createFakeRequest(listOf(request), extraRequestParameters, requireSurfacesForAllStreams)
+            createFakeRequest(listOf(request), defaultParameters, requiredParameters)
 
         if (rejectRequests || closeInvoked) {
             check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
@@ -88,11 +91,11 @@
 
     override fun submit(
         requests: List<Request>,
-        extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
-        requireSurfacesForAllStreams: Boolean
+        defaultParameters: Map<*, Any>,
+        requiredParameters: Map<*, Any>
     ): Boolean {
         val fakeRequest =
-            createFakeRequest(requests, extraRequestParameters, requireSurfacesForAllStreams)
+            createFakeRequest(requests, defaultParameters, requiredParameters)
         if (rejectRequests || closeInvoked) {
             check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
             return false
@@ -106,11 +109,11 @@
 
     override fun setRepeating(
         request: Request,
-        extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
-        requireSurfacesForAllStreams: Boolean
+        defaultParameters: Map<*, Any>,
+        requiredParameters: Map<*, Any>
     ): Boolean {
         val fakeRequest =
-            createFakeRequest(listOf(request), extraRequestParameters, requireSurfacesForAllStreams)
+            createFakeRequest(listOf(request), defaultParameters, requiredParameters)
         if (rejectRequests || closeInvoked) {
             check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
             return false
@@ -145,13 +148,29 @@
 
     private fun createFakeRequest(
         burst: List<Request>,
-        extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
-        requireStreams: Boolean
+        defaultParameters: Map<*, Any>,
+        requiredParameters: Map<*, Any>
     ): FakeRequest {
-        val parameterMap = mutableMapOf<CaptureRequest.Key<*>, Any>()
-        parameterMap.putAll(graphState3A.readState())
-        parameterMap.putAll(extraRequestParameters)
-        return FakeRequest(burst, parameterMap, requireStreams)
+        val internalParameters = graphState3A.readState()
+        val parameterMap = mutableMapOf<Any, Any>()
+        for ((k, v) in defaultParameters) {
+            if (k != null) {
+                parameterMap[k] = v
+            }
+        }
+        parameterMap.putAll(internalParameters)
+        for ((k, v) in requiredParameters) {
+            if (k != null) {
+                parameterMap[k] = v
+            }
+        }
+        return FakeRequest(
+            burst,
+            defaultParameters,
+            internalParameters,
+            requiredParameters,
+            parameterMap
+        )
     }
 }
 
diff --git a/camera/camera-video/build.gradle b/camera/camera-video/build.gradle
index 36ff47c..0463fe0 100644
--- a/camera/camera-video/build.gradle
+++ b/camera/camera-video/build.gradle
@@ -45,6 +45,7 @@
         exclude group: "androidx.camera", module: "camera-core"
     }
 
+    androidTestImplementation project(path: ':camera:camera-camera2')
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_CORE)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
diff --git a/camera/camera-video/src/androidTest/AndroidManifest.xml b/camera/camera-video/src/androidTest/AndroidManifest.xml
index 9f27ec7..7f7f316 100644
--- a/camera/camera-video/src/androidTest/AndroidManifest.xml
+++ b/camera/camera-video/src/androidTest/AndroidManifest.xml
@@ -17,4 +17,5 @@
     package="androidx.camera.video.test">
 
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.CAMERA" />
 </manifest>
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt
new file mode 100644
index 0000000..d925e3e
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2020 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.camera.video.internal.encoder
+
+import android.content.Context
+import android.graphics.SurfaceTexture
+import android.media.CamcorderProfile
+import android.media.MediaCodecInfo
+import android.media.MediaFormat
+import android.media.MediaRecorder
+import android.os.Build
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.Preview
+import androidx.camera.core.Preview.SurfaceProvider
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.SurfaceTextureProvider
+import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
+import androidx.core.content.ContextCompat
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.invocation.InvocationOnMock
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class VideoEncoderTest {
+
+    @get: Rule
+    var cameraRule: TestRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+    private var camera: CameraUseCaseAdapter? = null
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+
+    private lateinit var videoEncoderConfig: VideoEncoderConfig
+    private lateinit var videoEncoder: EncoderImpl
+    private lateinit var videoEncoderCallback: EncoderCallback
+    private lateinit var previewForVideoEncoder: Preview
+    private lateinit var preview: Preview
+    private lateinit var mainExecutor: Executor
+    private lateinit var encoderExecutor: Executor
+
+    @Before
+    fun setUp() {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+        // Same issue happened in new video encoder in pre-submit test. Bypass this test on
+        // CuttleFish API 29.
+        // TODO(b/168175357): Fix VideoCaptureTest problems on CuttleFish API 29
+        assumeFalse(
+            "Cuttlefish has MediaCodec dequeueInput/Output buffer fails issue. Unable to test.",
+            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
+        )
+
+        val cameraXConfig: CameraXConfig = Camera2Config.defaultConfig()
+        CameraX.initialize(context, cameraXConfig).get()
+
+        mainExecutor = ContextCompat.getMainExecutor(context)
+        encoderExecutor = CameraXExecutors.ioExecutor()
+
+        previewForVideoEncoder = Preview.Builder().build()
+        // Binding one more preview use case to create a surface texture, this is for testing on
+        // Pixel API 26, it needs a surface texture at least.
+        preview = Preview.Builder().build()
+
+        camera = CameraUtil.createCameraAndAttachUseCase(
+            context,
+            cameraSelector,
+            previewForVideoEncoder,
+            preview
+        )
+
+        initVideoEncoder()
+
+        instrumentation.runOnMainSync {
+            preview.setSurfaceProvider(
+                getSurfaceProvider()
+            )
+        }
+    }
+
+    @After
+    fun tearDown() {
+        // Since the mVideoEncoder is late initialized, check the status before end test.
+        if (this::videoEncoder.isInitialized) {
+            videoEncoder.release()
+        }
+
+        camera?.apply {
+            instrumentation.runOnMainSync {
+                removeUseCases(setOf(previewForVideoEncoder, preview))
+            }
+        }
+
+        // Ensure all cameras are released for the next test
+        CameraX.shutdown()[10, TimeUnit.SECONDS]
+    }
+
+    @Test
+    fun canRestartVideoEncoder() {
+        for (i in 0..3) {
+            clearInvocations(videoEncoderCallback)
+
+            videoEncoder.start()
+            val inOrder = inOrder(videoEncoderCallback)
+            inOrder.verify(videoEncoderCallback, timeout(5000L)).onEncodeStart()
+            inOrder.verify(videoEncoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+
+            videoEncoder.stop()
+
+            inOrder.verify(videoEncoderCallback, timeout(5000L)).onEncodeStop()
+        }
+    }
+
+    @Test
+    fun canPauseResumeVideoEncoder() {
+        videoEncoder.start()
+
+        verify(videoEncoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+
+        videoEncoder.pause()
+
+        // Since there is no exact event to know the encoder is paused, wait for a while until no
+        // callback.
+        verify(videoEncoderCallback, noInvocation(3000L, 10000L)).onEncodedData(any())
+
+        clearInvocations(videoEncoderCallback)
+
+        videoEncoder.start()
+
+        verify(videoEncoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+    }
+
+    @Test
+    fun canPauseStopStartVideoEncoder() {
+        videoEncoder.start()
+
+        verify(videoEncoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+
+        videoEncoder.pause()
+
+        // Since there is no exact event to know the encoder is paused, wait for a while until no
+        // callback.
+        verify(videoEncoderCallback, noInvocation(3000L, 10000L)).onEncodedData(any())
+
+        videoEncoder.stop()
+
+        verify(videoEncoderCallback, timeout(5000L)).onEncodeStop()
+
+        clearInvocations(videoEncoderCallback)
+
+        videoEncoder.start()
+
+        verify(videoEncoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+    }
+
+    private fun initVideoEncoder() {
+        val cameraId: Int = (camera?.cameraInfo as CameraInfoInternal).cameraId.toInt()
+
+        val profile: CamcorderProfile = when {
+            CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P) -> {
+                CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P)
+            }
+            CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P) -> {
+                CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P)
+            }
+            else -> {
+                CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW)
+            }
+        }
+
+        videoEncoderConfig = VideoEncoderConfig.builder()
+            .setBitrate(profile.videoBitRate)
+            .setColorFormat(MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
+            .setFrameRate(profile.videoFrameRate)
+            .setIFrameInterval(1)
+            .setMimeType(getMimeTypeString(profile.videoCodec))
+            .setResolution(Size(profile.videoFrameWidth, profile.videoFrameHeight))
+            .build()
+
+        // init video encoder
+        videoEncoderCallback = mock(EncoderCallback::class.java)
+        doAnswer { args: InvocationOnMock ->
+            val encodedData: EncodedData = args.getArgument(0)
+            encodedData.close()
+            null
+        }.`when`(videoEncoderCallback).onEncodedData(any())
+
+        videoEncoder = EncoderImpl(
+            encoderExecutor,
+            videoEncoderConfig
+        )
+
+        videoEncoder.setEncoderCallback(videoEncoderCallback, CameraXExecutors.directExecutor())
+
+        (videoEncoder.input as Encoder.SurfaceInput).setOnSurfaceUpdateListener(
+            mainExecutor,
+            { surface: Surface ->
+                previewForVideoEncoder.setSurfaceProvider { request: SurfaceRequest ->
+                    request.provideSurface(
+                        surface,
+                        CameraXExecutors.directExecutor(),
+                        {
+                            surface.release()
+                        }
+                    )
+                }
+            }
+        )
+    }
+
+    private fun getSurfaceProvider(): SurfaceProvider? {
+        return SurfaceTextureProvider.createSurfaceTextureProvider(object : SurfaceTextureCallback {
+            override fun onSurfaceTextureReady(surfaceTexture: SurfaceTexture, resolution: Size) {
+                // No-op
+            }
+
+            override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
+                surfaceTexture.release()
+            }
+        })
+    }
+
+    private fun getMimeTypeString(encoder: Int): String {
+        return when (encoder) {
+            MediaRecorder.VideoEncoder.H263 -> MediaFormat.MIMETYPE_VIDEO_H263
+            MediaRecorder.VideoEncoder.H264 -> MediaFormat.MIMETYPE_VIDEO_AVC
+            MediaRecorder.VideoEncoder.HEVC -> MediaFormat.MIMETYPE_VIDEO_HEVC
+            MediaRecorder.VideoEncoder.MPEG_4_SP -> MediaFormat.MIMETYPE_VIDEO_MPEG4
+            MediaRecorder.VideoEncoder.VP8 -> MediaFormat.MIMETYPE_VIDEO_VP8
+            MediaRecorder.VideoEncoder.DEFAULT -> MediaFormat.MIMETYPE_VIDEO_AVC
+            else -> MediaFormat.MIMETYPE_VIDEO_AVC
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
new file mode 100644
index 0000000..8ea2b97
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020 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.camera.video.internal.compat.quirk;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.impl.Quirks;
+
+/**
+ * Provider of video capture related quirks, which are used for device or API level specific
+ * workarounds.
+ * <p>Video related quirks that include depending on API level
+ * ({@link android.os.Build.VERSION#SDK_INT}) or specific devices.
+ * <p>Video specific quirks are lazily loaded, i.e. They are loaded the first time they're needed.
+ */
+public class DeviceQuirks {
+    @NonNull
+    private static final Quirks QUIRKS;
+
+    static {
+        QUIRKS = new Quirks(DeviceQuirksLoader.loadQuirks());
+    }
+
+    private DeviceQuirks() {
+    }
+
+    /** Returns all video specific quirks loaded on the current device. */
+    @NonNull
+    public static Quirks getAll() {
+        return QUIRKS;
+    }
+
+    /**
+     * Retrieves a specific video {@link Quirk} instance given its type.
+     *
+     * @param quirkClass The type of video quirk to retrieve.
+     * @return A video {@link Quirk} instance of the provided type, or {@code null} if it isn't
+     * found.
+     */
+    @Nullable
+    public static <T extends Quirk> T get(@NonNull final Class<T> quirkClass) {
+        return QUIRKS.get(quirkClass);
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
new file mode 100644
index 0000000..a394bc6
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020 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.camera.video.internal.compat.quirk;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Loads all video specific quirks required for the current device.
+ */
+public class DeviceQuirksLoader {
+
+    private DeviceQuirksLoader() {
+    }
+
+    /**
+     * Goes through all defined video related quirks, and returns those that should be loaded
+     * on the current device.
+     */
+    @NonNull
+    static List<Quirk> loadQuirks() {
+        final List<Quirk> quirks = new ArrayList<>();
+
+        // Load all video specific quirks
+        if (ExcludeKeyFrameRateInFindEncoderQuirk.load()) {
+            quirks.add(new ExcludeKeyFrameRateInFindEncoderQuirk());
+        }
+
+        return quirks;
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeKeyFrameRateInFindEncoderQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeKeyFrameRateInFindEncoderQuirk.java
new file mode 100644
index 0000000..ae4c469
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeKeyFrameRateInFindEncoderQuirk.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 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.camera.video.internal.compat.quirk;
+
+import android.media.MediaFormat;
+import android.os.Build;
+
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * Quirk requiring that the frame rate is not set on the MediaFormat during codec selection.
+ * <p>For API 21, before using
+ * {@link android.media.MediaCodecList#findEncoderForFormat(MediaFormat)}, it needs to reset
+ * frame rate config to null. But in the MediaCode.configure() phase on API 21, the MediaFormat
+ * should include frame rate value.
+ *
+ * @see <a href="https://developer.android
+ * .com/reference/android/media/MediaCodec#creation">MediaCodec's creation</a>
+ */
+public class ExcludeKeyFrameRateInFindEncoderQuirk implements Quirk {
+
+    static boolean load() {
+        return Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP;
+    }
+}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTesting.kt b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/package-info.java
similarity index 65%
rename from compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTesting.kt
rename to camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/package-info.java
index d5ffe26..e0f4cbe 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTesting.kt
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/package-info.java
@@ -14,12 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.test
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.camera.video.internal.compat.quirk;
 
-@RequiresOptIn("This testing API is experimental and is likely to be changed or removed entirely")
-annotation class ExperimentalTesting
-
-@RequiresOptIn(
-    "This is internal API for Compose modules that may change frequently and without warning."
-)
-annotation class InternalTestingApi
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index acd7535..4d39192 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -28,6 +28,7 @@
 
 import android.annotation.SuppressLint;
 import android.media.MediaCodec;
+import android.media.MediaCodecList;
 import android.media.MediaFormat;
 import android.os.Build;
 import android.os.Bundle;
@@ -42,6 +43,7 @@
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.camera.video.internal.workaround.EncoderFinder;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
 import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
 import androidx.core.util.Preconditions;
@@ -150,6 +152,7 @@
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     InternalState mState;
 
+    final EncoderFinder mEncoderFinder = new EncoderFinder();
     /**
      * Creates the encoder with a {@link EncoderConfig}
      *
@@ -188,14 +191,8 @@
             throw new InvalidConfigException("Unknown encoder config type");
         }
 
-        try {
-            mMediaCodec = MediaCodec.createEncoderByType(encoderConfig.getMimeType());
-        } catch (IOException e) {
-            throw new InvalidConfigException(
-                    "Unsupported mime type: " + encoderConfig.getMimeType(), e);
-        }
-
         mMediaFormat = encoderConfig.toMediaFormat();
+        mMediaCodec = selectMediaCodecEncoder(mMediaFormat);
 
         try {
             reset();
@@ -423,7 +420,7 @@
     @ExecutedBy("mEncoderExecutor")
     private void updatePauseToMediaCodec(boolean paused) {
         Bundle bundle = new Bundle();
-        bundle.putBoolean(MediaCodec.PARAMETER_KEY_SUSPEND, paused);
+        bundle.putInt(MediaCodec.PARAMETER_KEY_SUSPEND, paused ? 1 : 0);
         mMediaCodec.setParameters(bundle);
     }
 
@@ -563,6 +560,26 @@
         }
     }
 
+    @NonNull
+    private MediaCodec selectMediaCodecEncoder(@NonNull MediaFormat mediaFormat)
+            throws InvalidConfigException {
+        MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
+        String encoderName;
+
+        encoderName = mEncoderFinder.findEncoderForFormat(mediaFormat, mediaCodecList);
+
+        MediaCodec codec;
+
+        try {
+            codec = MediaCodec.createByCodecName(encoderName);
+        } catch (IOException | NullPointerException | IllegalArgumentException e) {
+            throw new InvalidConfigException("Encoder cannot created: " + encoderName, e);
+        }
+        Logger.i(TAG, "Selected encoder: " + codec.getName());
+
+        return codec;
+    }
+
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     @ExecutedBy("mEncoderExecutor")
     @NonNull
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/EncoderFinder.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/EncoderFinder.java
new file mode 100644
index 0000000..e510147
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/EncoderFinder.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2020 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.camera.video.internal.workaround;
+
+import android.media.MediaCodecList;
+import android.media.MediaFormat;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.video.internal.compat.quirk.ExcludeKeyFrameRateInFindEncoderQuirk;
+
+/**
+ * Workaround to fix the selection of video encoder by MediaFormat on API 21.
+ *
+ * @see ExcludeKeyFrameRateInFindEncoderQuirk
+ */
+public class EncoderFinder {
+    private final boolean mShouldRemoveKeyFrameRate;
+
+    public EncoderFinder() {
+        final ExcludeKeyFrameRateInFindEncoderQuirk quirk =
+                DeviceQuirks.get(ExcludeKeyFrameRateInFindEncoderQuirk.class);
+
+        mShouldRemoveKeyFrameRate = (quirk != null);
+    }
+
+    /**
+     * Selects an encoder by a given MediaFormat.
+     *
+     * <p>There is one particular case when get a video encoder on API 21.
+     */
+    @Nullable
+    public String findEncoderForFormat(@NonNull MediaFormat mediaFormat,
+            @NonNull MediaCodecList mediaCodecList) {
+        // If the frame rate value is assigned, keep it and restore it later.
+        String encoderName;
+
+        if (mShouldRemoveKeyFrameRate && mediaFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) {
+            int tempFrameRate = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE);
+            // Reset frame rate value in API 21.
+            mediaFormat.setString(MediaFormat.KEY_FRAME_RATE, null);
+            encoderName = mediaCodecList.findEncoderForFormat(mediaFormat);
+            // Restore the frame rate value.
+            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, tempFrameRate);
+        } else {
+            encoderName = mediaCodecList.findEncoderForFormat(mediaFormat);
+        }
+
+        return encoderName;
+    }
+
+}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTesting.kt b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/package-info.java
similarity index 65%
copy from compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTesting.kt
copy to camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/package-info.java
index d5ffe26..29928ac 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTesting.kt
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/package-info.java
@@ -14,12 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.test
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.camera.video.internal.workaround;
 
-@RequiresOptIn("This testing API is experimental and is likely to be changed or removed entirely")
-annotation class ExperimentalTesting
-
-@RequiresOptIn(
-    "This is internal API for Compose modules that may change frequently and without warning."
-)
-annotation class InternalTestingApi
+import androidx.annotation.RestrictTo;
diff --git a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt
index 9d18861..c5bb347 100644
--- a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt
+++ b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt
@@ -17,9 +17,7 @@
 package androidx.camera.integration.camera2.pipe
 
 import android.graphics.ImageFormat
-import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraDevice
-import android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
 import android.media.ImageReader
 import android.os.Handler
 import android.util.Log
@@ -31,9 +29,10 @@
 import androidx.camera.camera2.pipe.CameraPipe
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.RequestTemplate
-import androidx.camera.camera2.pipe.StreamConfig
+import androidx.camera.camera2.pipe.CameraStream.Config
+import androidx.camera.camera2.pipe.OutputStream
 import androidx.camera.camera2.pipe.StreamFormat
-import androidx.camera.camera2.pipe.StreamType
+import androidx.camera.camera2.pipe.core.Debug
 import kotlin.math.absoluteValue
 
 private const val defaultWidth = 960
@@ -42,9 +41,9 @@
 private const val defaultAspectRatio = defaultWidth.toDouble() / defaultHeight.toDouble()
 
 class SimpleCamera(
-    private val cameraId: CameraId,
-    private val cameraMetadata: CameraMetadata,
+    private val cameraConfig: CameraGraph.Config,
     private val cameraGraph: CameraGraph,
+    private val cameraMetadata: CameraMetadata,
     private val imageReader: ImageReader
 ) {
     companion object {
@@ -77,18 +76,15 @@
 
             Log.i("CXCP-App", "Selected $yuvSize as the YUV output size")
 
-            val yuvStreamConfig = StreamConfig(
-                yuvSize,
-                StreamFormat.YUV_420_888,
-                cameraId,
-                StreamType.SURFACE
-            )
-
-            val viewfinderStreamConfig = StreamConfig(
+            val viewfinderStreamConfig = Config.create(
                 yuvSize,
                 StreamFormat.UNKNOWN,
-                cameraId,
-                StreamType.SURFACE_VIEW
+                outputType = OutputStream.OutputType.SURFACE_VIEW
+            )
+
+            val yuvStreamConfig = Config.create(
+                yuvSize,
+                StreamFormat.YUV_420_888
             )
 
             val config = CameraGraph.Config(
@@ -97,15 +93,17 @@
                     viewfinderStreamConfig,
                     yuvStreamConfig
                 ),
-                listeners = listeners,
-                template = RequestTemplate(CameraDevice.TEMPLATE_PREVIEW)
+                defaultListeners = listeners,
+                defaultTemplate = RequestTemplate(CameraDevice.TEMPLATE_PREVIEW)
             )
 
             val cameraGraph = cameraPipe.create(config)
 
             val viewfinderStream = cameraGraph.streams[viewfinderStreamConfig]!!
+            val viewfinderOutput = viewfinderStream.outputs.single()
+
             viewfinder.configure(
-                viewfinderStream.size,
+                viewfinderOutput.size,
                 object : Viewfinder.SurfaceListener {
                     override fun onSurfaceChanged(surface: Surface?, size: Size?) {
                         Log.i("CXCP-App", "Viewfinder surface changed to $surface at $size")
@@ -114,6 +112,16 @@
                 }
             )
             val yuvStream = cameraGraph.streams[yuvStreamConfig]!!
+            val yuvOutput = yuvStream.outputs.single()
+
+            val imageReader = ImageReader.newInstance(
+                yuvOutput.size.width,
+                yuvOutput.size.height,
+                yuvOutput.format.value,
+                10
+            )
+            cameraGraph.setSurface(yuvStream.id, imageReader.surface)
+
             cameraGraph.acquireSessionOrNull()!!.use {
                 it.setRepeating(
                     Request(
@@ -122,14 +130,12 @@
                 )
             }
 
-            val imageReader = ImageReader.newInstance(
-                yuvSize.width,
-                yuvSize.height,
-                ImageFormat.YUV_420_888,
-                10
+            return SimpleCamera(
+                config,
+                cameraGraph,
+                cameraMetadata,
+                imageReader
             )
-            cameraGraph.setSurface(yuvStream.id, imageReader.surface)
-            return SimpleCamera(cameraId, cameraMetadata, cameraGraph, imageReader)
         }
 
         private fun Size.aspectRatio(): Double {
@@ -177,36 +183,6 @@
         imageReader.close()
     }
 
-    fun cameraInfoString(): String {
-        val lensFacing = when (cameraMetadata[CameraCharacteristics.LENS_FACING]) {
-            CameraCharacteristics.LENS_FACING_FRONT -> "Front"
-            CameraCharacteristics.LENS_FACING_BACK -> "Back"
-            CameraCharacteristics.LENS_FACING_EXTERNAL -> "External"
-            else -> "Unknown"
-        }
-
-        val capabilities = cameraMetadata[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES]
-        val cameraType = if (capabilities != null &&
-            capabilities.contains(REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)
-        ) {
-            "Logical"
-        } else {
-            "Physical"
-        }
-
-        return StringBuilder().apply {
-            append("$cameraGraph (Camera ${cameraId.value})\n")
-            append("  Facing:    $lensFacing ($cameraType)\n")
-            append("Streams:")
-            for (stream in cameraGraph.streams) {
-                append("\n  ")
-                append(stream.value.id.toString().padEnd(12, ' '))
-                append(stream.value.size.toString().padEnd(12, ' '))
-                append(stream.value.format.name.padEnd(16, ' '))
-                append(stream.value.type.toString().padEnd(16, ' '))
-            }
-
-            // TODO: Add static configuration info.
-        }.toString()
-    }
+    fun cameraInfoString(): String =
+        Debug.formatCameraGraphProperties(cameraMetadata, cameraConfig, cameraGraph)
 }
diff --git a/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/SingleValueAnimationTest.kt b/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/SingleValueAnimationTest.kt
index e3cd31a..419c6e8 100644
--- a/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/SingleValueAnimationTest.kt
+++ b/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/SingleValueAnimationTest.kt
@@ -29,7 +29,7 @@
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.lerp
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.Bounds
 import androidx.compose.ui.unit.DpOffset
@@ -44,7 +44,7 @@
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class SingleValueAnimationTest {
 
     @get:Rule
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonotonicFrameAnimationClock.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonotonicFrameAnimationClock.kt
index 9402702..ffe6bb5 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonotonicFrameAnimationClock.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonotonicFrameAnimationClock.kt
@@ -20,6 +20,8 @@
 import androidx.compose.runtime.dispatch.MonotonicFrameClock
 import androidx.compose.runtime.dispatch.withFrameMillis
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
 
@@ -59,7 +61,10 @@
         // until unsubscribe for that observer is called.
         @Suppress("DEPRECATION_ERROR")
         synchronized(observers) {
-            observers[observer] = scope.launch {
+            // Start the coroutine undispatched to make sure it is awaiting
+            // the next frame before the frame clock sends that next frame
+            @OptIn(ExperimentalCoroutinesApi::class)
+            observers[observer] = scope.launch(start = CoroutineStart.UNDISPATCHED) {
                 val clock = coroutineContext[MonotonicFrameClock] ?: DefaultMonotonicFrameClock
                 // ManualAnimationClock might send the current time when a subscriber subscribes.
                 // ManualFrameClock doesn't, because there's no concept of subscription. Fix this
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
index f294292..a808fc3 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
@@ -30,7 +30,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.AmbientDensity
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
@@ -47,7 +47,7 @@
 
 @RunWith(AndroidJUnit4::class)
 @LargeTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class AnimatedVisibilityTest {
 
     @get:Rule
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
index 8968251..4ba104e 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
@@ -31,7 +31,7 @@
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntSize
@@ -52,7 +52,7 @@
 
 @RunWith(AndroidJUnit4::class)
 @LargeTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class AnimationModifierTest {
 
     @get:Rule
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/CrossfadeTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/CrossfadeTest.kt
index a2a81cb..16e9d50 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/CrossfadeTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/CrossfadeTest.kt
@@ -23,7 +23,7 @@
 import androidx.compose.runtime.onDispose
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -35,7 +35,7 @@
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class CrossfadeTest {
 
     @get:Rule
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
index db9c668..734012b 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
@@ -96,9 +96,29 @@
                     TextBox(text = AppState.wndTitle.value)
                 }
                 Row {
-                    Button(color = Color(232, 182, 109), size = IntSize(16, 16))
+                    Button(
+                        color = Color(210, 210, 210),
+                        size = IntSize(16, 16),
+                        onClick = {
+                            AppManager.focusedWindow?.makeFullscreen()
+                        }
+                    )
+                    Spacer(modifier = Modifier.width(30.dp))
+                    Button(
+                        color = Color(232, 182, 109),
+                        size = IntSize(16, 16),
+                        onClick = {
+                            AppManager.focusedWindow?.minimize()
+                        }
+                    )
                     Spacer(modifier = Modifier.width(3.dp))
-                    Button(color = Color(150, 232, 150), size = IntSize(16, 16))
+                    Button(
+                        color = Color(150, 232, 150),
+                        size = IntSize(16, 16),
+                        onClick = {
+                            AppManager.focusedWindow?.maximize()
+                        }
+                    )
                     Spacer(modifier = Modifier.width(3.dp))
                     Button(
                         onClick = { AppManager.exit() },
@@ -130,6 +150,9 @@
                                         AppManager.focusedWindow?.close()
                                     }
                                 )
+                                onDispose {
+                                    println("Dispose composition")
+                                }
                             }
                         },
                         color = Color(26, 198, 188)
@@ -160,15 +183,14 @@
                         .fillMaxWidth()
                 ) {
                     ContextMenu()
-                    Spacer(modifier = Modifier.height(30.dp))
-                    Spacer(modifier = Modifier.height(60.dp))
+                    Spacer(modifier = Modifier.height(100.dp))
                     Row {
                         Checkbox(
                             checked = AppState.undecorated.value,
                             onCheckedChange = {
                                 AppState.undecorated.value = !AppState.undecorated.value
                             },
-                            modifier = Modifier.height(30.dp).padding(start = 20.dp)
+                            modifier = Modifier.height(35.dp).padding(start = 20.dp)
                         )
                         Spacer(modifier = Modifier.width(5.dp))
                         TextBox(text = "- undecorated")
@@ -320,7 +342,7 @@
     text: String = "",
     onClick: () -> Unit = {},
     color: Color = Color(10, 162, 232),
-    size: IntSize = IntSize(150, 30)
+    size: IntSize = IntSize(200, 35)
 ) {
     val buttonHover = remember { mutableStateOf(false) }
     Button(
@@ -371,7 +393,8 @@
 
     Surface(
         modifier = Modifier
-            .padding(start = 4.dp, top = 2.dp),
+            .padding(start = 4.dp, top = 2.dp)
+            .clickable(onClick = { showMenu.value = true }),
         color = Color(255, 255, 255, 40),
         shape = RoundedCornerShape(4.dp)
     ) {
@@ -380,9 +403,8 @@
                 TextBox(
                     text = "Selected: ${items[selectedIndex.value]}",
                     modifier = Modifier
-                        .height(26.dp)
+                        .height(35.dp)
                         .padding(start = 4.dp, end = 4.dp)
-                        .clickable(onClick = { showMenu.value = true })
                 )
             },
             expanded = showMenu.value,
@@ -405,7 +427,7 @@
 @Composable
 fun RadioButton(text: String, state: MutableState<Boolean>) {
     Box(
-        modifier = Modifier.height(30.dp),
+        modifier = Modifier.height(35.dp),
         contentAlignment = Alignment.Center
     ) {
         RadioButton(
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/Main.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/Main.kt
index 4269d43..3fc4f935 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/Main.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/Main.kt
@@ -71,6 +71,7 @@
             ),
             Menu(
                 "About",
+                MenuItems.IsFullscreen,
                 MenuItems.About,
                 MenuItems.Update
             )
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/MenuItems.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/MenuItems.kt
index 0d41ab0..e43c1d2 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/MenuItems.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/MenuItems.kt
@@ -60,4 +60,12 @@
         },
         shortcut = KeyStroke(Key.U)
     )
+
+    val IsFullscreen = MenuItem(
+        name = "Is fullscreen mode",
+        onClick = {
+            println("Fullscreen mode: ${AppManager.focusedWindow?.isFullscreen}")
+        },
+        shortcut = KeyStroke(Key.F)
+    )
 }
\ No newline at end of file
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
index bfb6073..d3826cd 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
@@ -36,6 +36,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.onDispose
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -70,6 +71,9 @@
     // setting the content
     composePanel.setContent {
         ComposeContent()
+        onDispose {
+            println("Dispose composition")
+        }
     }
 
     val window = JFrame()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
index b163cd1..8e0ee4ea 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -42,7 +42,7 @@
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.center
 import androidx.compose.ui.test.down
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -90,7 +90,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_horizontalScroll() = runBlockingWithManualClock { clock ->
         var total = 0f
         val controller = ScrollableController(
@@ -146,7 +146,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_verticalScroll() = runBlockingWithManualClock { clock ->
         var total = 0f
         val controller = ScrollableController(
@@ -202,7 +202,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_startStop_notify() = runBlockingWithManualClock(true) { clock ->
         var startTrigger = 0f
         var stopTrigger = 0f
@@ -248,7 +248,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_disabledWontCallLambda() = runBlockingWithManualClock(true) { clock ->
         val enabled = mutableStateOf(true)
         var total = 0f
@@ -294,7 +294,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_velocityProxy() = runBlockingWithManualClock { clock ->
         var velocityTriggered = 0f
         var total = 0f
@@ -342,7 +342,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_startWithoutSlop_ifFlinging() = runBlockingWithManualClock { clock ->
         var total = 0f
         val controller = ScrollableController(
@@ -386,7 +386,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_cancel_callsDragStop() = runBlocking {
         var total by mutableStateOf(0f)
         var dragStopped = 0f
@@ -425,7 +425,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_snappingScrolling() = runBlocking {
         var total = 0f
         val controller = ScrollableController(
@@ -450,7 +450,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_explicitDisposal() = runBlockingWithManualClock { clock ->
         val disposed = mutableStateOf(false)
         var total = 0f
@@ -491,7 +491,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_nestedDrag() = runBlockingWithManualClock { clock ->
         var innerDrag = 0f
         var outerDrag = 0f
@@ -560,7 +560,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_nestedFling() = runBlockingWithManualClock { clock ->
         var innerDrag = 0f
         var outerDrag = 0f
@@ -631,7 +631,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_nestedScrollAbove_respectsPreConsumption() =
         runBlockingWithManualClock { clock ->
             var value = 0f
@@ -699,7 +699,7 @@
         }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_nestedScrollAbove_proxiesPostCycles() =
         runBlockingWithManualClock { clock ->
             var value = 0f
@@ -776,7 +776,7 @@
         }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_nestedScrollBelow_listensDispatches() =
         runBlockingWithManualClock { clock ->
             var value = 0f
@@ -848,7 +848,7 @@
         }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_interactionState() = runBlocking {
         val interactionState = InteractionState()
         var total = 0f
@@ -894,7 +894,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun scrollable_interactionState_resetWhenDisposed() = runBlocking {
         val interactionState = InteractionState()
         var emitScrollableBox by mutableStateOf(true)
@@ -985,7 +985,7 @@
         }
     }
 
-    @ExperimentalTesting
+    @ExperimentalTestApi
     private suspend fun advanceClockWhileAwaitersExist(clock: ManualFrameClock) {
         rule.awaitIdle()
         yield()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ZoomableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ZoomableTest.kt
index d658268..509facc 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ZoomableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ZoomableTest.kt
@@ -27,7 +27,7 @@
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.center
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -51,7 +51,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class ZoomableTest {
     @get:Rule
     val rule = createComposeRule()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
index f5b69ad..4bfd688 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
@@ -33,7 +33,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.hasSetTextAction
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -54,7 +54,7 @@
 import java.util.concurrent.TimeUnit
 
 @LargeTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class TextFieldCursorTest {
 
     @get:Rule
diff --git a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
index c2f7983..010d2cb 100644
--- a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
+++ b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
@@ -38,7 +38,7 @@
 import androidx.compose.ui.platform.DesktopPlatform
 import androidx.compose.ui.platform.DesktopPlatformAmbient
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
 import androidx.compose.ui.test.down
 import androidx.compose.ui.test.junit4.ComposeTestRule
@@ -61,13 +61,13 @@
 import org.junit.Test
 
 @Suppress("WrapUnaryOperator")
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class ScrollbarTest {
     @get:Rule
     val rule = createComposeRule()
 
     // don't inline, surface controls canvas life time
-    private val surface = Surface.makeRasterN32Premul(100, 100)
+    private val surface = Surface.makeRasterN32Premul(100, 100)!!
     private val canvas = surface.canvas
 
     @Test
@@ -468,4 +468,4 @@
         DesktopPlatformAmbient provides DesktopPlatform.MacOS,
         content = content
     )
-}
\ No newline at end of file
+}
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
index 7d08b8b..061ca60 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
@@ -33,7 +33,7 @@
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
 import androidx.compose.ui.test.junit4.DisableTransitionsTestRule
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
@@ -128,7 +128,7 @@
         val benchmarkRule = BenchmarkRule()
 
         override fun apply(base: Statement, description: Description?): Statement {
-            @OptIn(InternalTestingApi::class)
+            @OptIn(InternalTestApi::class)
             return RuleChain
                 .outerRule(DisableTransitionsTestRule())
                 .around(benchmarkRule)
diff --git a/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt b/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt
index e0f954c..f7fd132 100644
--- a/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt
+++ b/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt
@@ -26,7 +26,7 @@
 import androidx.compose.integration.demos.common.DemoCategory
 import androidx.compose.integration.demos.common.allDemos
 import androidx.compose.integration.demos.common.allLaunchableDemos
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.SemanticsNodeInteractionCollection
 import androidx.compose.ui.test.assertTextEquals
 import androidx.compose.ui.test.hasClickAction
@@ -55,7 +55,7 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class DemoTest {
     @get:Rule
     val rule = createAndroidComposeRule<DemoActivity>()
diff --git a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/CheckboxesInRowsTest.kt b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/CheckboxesInRowsTest.kt
index ef71158..2d4b960 100644
--- a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/CheckboxesInRowsTest.kt
+++ b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/CheckboxesInRowsTest.kt
@@ -20,7 +20,7 @@
 import androidx.compose.testutils.assertMeasureSizeIsPositive
 import androidx.compose.testutils.assertNoPendingChanges
 import androidx.compose.testutils.forGivenTestCase
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.test.filters.MediumTest
 import androidx.ui.integration.test.material.CheckboxesInRowsTestCase
@@ -34,7 +34,7 @@
  */
 @MediumTest
 @RunWith(Parameterized::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class CheckboxesInRowsTest(private val numberOfCheckboxes: Int) {
 
     companion object {
diff --git a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ObservableThemeTest.kt b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ObservableThemeTest.kt
index f07c9e6..3fbead4 100644
--- a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ObservableThemeTest.kt
+++ b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ObservableThemeTest.kt
@@ -33,7 +33,7 @@
 import androidx.compose.testutils.doFramesUntilNoChangesPending
 import androidx.compose.testutils.forGivenTestCase
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -49,7 +49,7 @@
  */
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class ObservableThemeTest {
     @get:Rule
     val composeTestRule = createAndroidComposeRule<ComponentActivity>()
diff --git a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnSharedModelTest.kt b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnSharedModelTest.kt
index 8de27ae..9c3e556 100644
--- a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnSharedModelTest.kt
+++ b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnSharedModelTest.kt
@@ -20,7 +20,7 @@
 import androidx.compose.testutils.assertMeasureSizeIsPositive
 import androidx.compose.testutils.assertNoPendingChanges
 import androidx.compose.testutils.forGivenTestCase
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.test.filters.MediumTest
 import androidx.ui.integration.test.foundation.RectsInColumnSharedModelTestCase
@@ -34,7 +34,7 @@
  */
 @MediumTest
 @RunWith(Parameterized::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class RectsInColumnSharedModelTest(private val numberOfRectangles: Int) {
 
     companion object {
diff --git a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnTest.kt b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnTest.kt
index d77f08c..f6f4944 100644
--- a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnTest.kt
+++ b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnTest.kt
@@ -20,7 +20,7 @@
 import androidx.compose.testutils.assertMeasureSizeIsPositive
 import androidx.compose.testutils.assertNoPendingChanges
 import androidx.compose.testutils.forGivenTestCase
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.test.filters.MediumTest
 import androidx.ui.integration.test.foundation.RectsInColumnTestCase
@@ -34,7 +34,7 @@
  */
 @MediumTest
 @RunWith(Parameterized::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class RectsInColumnTest(private val numberOfRectangles: Int) {
 
     companion object {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
index 4d647fa..737fb0f 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
@@ -29,7 +29,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -44,7 +44,7 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterialApi::class, ExperimentalTesting::class)
+@OptIn(ExperimentalMaterialApi::class, ExperimentalTestApi::class)
 class BottomNavigationScreenshotTest {
 
     @get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
index 1f907e6..86bc79e 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
@@ -21,7 +21,7 @@
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.center
 import androidx.compose.ui.test.down
@@ -42,7 +42,7 @@
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class ButtonScreenshotTest {
 
     @get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
index 23b19e1..616c84a 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
@@ -25,7 +25,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.state.ToggleableState
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.center
 import androidx.compose.ui.test.down
@@ -46,7 +46,7 @@
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class CheckboxScreenshotTest {
 
     @get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
index 2bd7a2d..b4c3dc2 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
@@ -46,7 +46,7 @@
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.ComposeTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -68,7 +68,7 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterialApi::class, ExperimentalTesting::class, ExperimentalRippleApi::class)
+@OptIn(ExperimentalMaterialApi::class, ExperimentalTestApi::class, ExperimentalRippleApi::class)
 class MaterialRippleThemeTest {
 
     @get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
index d048f77..9c678e3 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
@@ -28,7 +28,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.hasAnyDescendant
 import androidx.compose.ui.test.hasTestTag
 import androidx.compose.ui.test.isPopup
@@ -51,7 +51,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class MenuTest {
     @get:Rule
     val rule = createComposeRule()
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
index 5514d9d..c59785d 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
@@ -19,7 +19,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.AccessibilityRangeInfo
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.assertHeightIsEqualTo
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertRangeInfoEquals
@@ -36,7 +36,7 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class ProgressIndicatorTest {
 
     private val ExpectedLinearWidth = 240.dp
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
index 747b65f..4ad4c06 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
@@ -25,7 +25,7 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.center
 import androidx.compose.ui.test.down
@@ -46,7 +46,7 @@
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class RadioButtonScreenshotTest {
 
     @get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
index 9b05fd8..e4c06ed 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
@@ -29,7 +29,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.AmbientLayoutDirection
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.center
 import androidx.compose.ui.test.down
@@ -52,7 +52,7 @@
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class SwitchScreenshotTest {
 
     @get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
index 694bbc6..844ac05 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
@@ -27,7 +27,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -42,7 +42,7 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterialApi::class, ExperimentalTesting::class)
+@OptIn(ExperimentalMaterialApi::class, ExperimentalTestApi::class)
 class TabScreenshotTest {
 
     @get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
index b35ec1e..37e62f8 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
@@ -47,7 +47,7 @@
 import androidx.compose.ui.node.Ref
 import androidx.compose.ui.platform.AmbientTextInputService
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -81,7 +81,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class OutlinedTextFieldTest {
     private val ExpectedMinimumTextFieldHeight = 56.dp
     private val ExpectedPadding = 16.dp
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
index d35d2bc9..5158ad4 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
@@ -57,7 +57,7 @@
 import androidx.compose.ui.platform.AmbientTextInputService
 import androidx.compose.ui.platform.AmbientView
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.assertHeightIsEqualTo
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.click
@@ -95,7 +95,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class TextFieldTest {
 
     private val ExpectedMinimumTextFieldHeight = 56.dp
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/ComposeBenchmarkRule.kt b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/ComposeBenchmarkRule.kt
index 26c1511..dda8985 100644
--- a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/ComposeBenchmarkRule.kt
+++ b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/ComposeBenchmarkRule.kt
@@ -23,8 +23,8 @@
 import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.benchmark.android.AndroidTestCase
 import androidx.compose.testutils.createAndroidComposeBenchmarkRunner
+import androidx.compose.ui.test.InternalTestApi
 import androidx.compose.ui.test.junit4.DisableTransitionsTestRule
-import androidx.compose.ui.test.InternalTestingApi
 import org.junit.rules.RuleChain
 import org.junit.rules.TestRule
 import org.junit.runner.Description
@@ -44,7 +44,7 @@
     val benchmarkRule = BenchmarkRule()
 
     override fun apply(base: Statement, description: Description?): Statement {
-        @OptIn(InternalTestingApi::class)
+        @OptIn(InternalTestApi::class)
         return RuleChain
             .outerRule(DisableTransitionsTestRule(!enableTransitions))
             .around(benchmarkRule)
diff --git a/compose/ui/ui-test-junit4/api/current.txt b/compose/ui/ui-test-junit4/api/current.txt
index f4d018a..a6f3662 100644
--- a/compose/ui/ui-test-junit4/api/current.txt
+++ b/compose/ui/ui-test-junit4/api/current.txt
@@ -2,13 +2,13 @@
 package androidx.compose.ui.test.junit4 {
 
   public final class AndroidAnimationClockTestRuleKt {
-    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
   }
 
   public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
     ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
     method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
-    method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public R getActivityRule();
     method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
     method public androidx.compose.ui.unit.Density getDensity();
@@ -41,7 +41,7 @@
   public final class AndroidSynchronizationKt {
   }
 
-  @androidx.compose.ui.test.ExperimentalTesting public interface AnimationClockTestRule extends org.junit.rules.TestRule {
+  @androidx.compose.ui.test.ExperimentalTestApi public interface AnimationClockTestRule extends org.junit.rules.TestRule {
     method public default void advanceClock(long milliseconds);
     method public androidx.compose.ui.test.TestAnimationClock getClock();
     method public default boolean isPaused();
@@ -52,7 +52,7 @@
   }
 
   public interface ComposeTestRule extends org.junit.rules.TestRule androidx.compose.ui.test.SemanticsNodeInteractionsProvider {
-    method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
     method public androidx.compose.ui.unit.Density getDensity();
     method public long getDisplaySize-YbymL2g();
@@ -67,7 +67,7 @@
     property public abstract long displaySize;
   }
 
-  @androidx.compose.ui.test.InternalTestingApi public final class DisableTransitionsTestRule implements org.junit.rules.TestRule {
+  @androidx.compose.ui.test.InternalTestApi public final class DisableTransitionsTestRule implements org.junit.rules.TestRule {
     ctor public DisableTransitionsTestRule(boolean disableTransitions);
     ctor public DisableTransitionsTestRule();
     method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
@@ -91,9 +91,9 @@
 
   public final class ComposeIdlingResourceKt {
     method @Deprecated public static void registerComposeWithEspresso();
-    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
     method @Deprecated public static void unregisterComposeFromEspresso();
-    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
   }
 
   public final class ComposeNotIdleException extends java.lang.Throwable {
diff --git a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
index f4d018a..a6f3662 100644
--- a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
@@ -2,13 +2,13 @@
 package androidx.compose.ui.test.junit4 {
 
   public final class AndroidAnimationClockTestRuleKt {
-    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
   }
 
   public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
     ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
     method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
-    method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public R getActivityRule();
     method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
     method public androidx.compose.ui.unit.Density getDensity();
@@ -41,7 +41,7 @@
   public final class AndroidSynchronizationKt {
   }
 
-  @androidx.compose.ui.test.ExperimentalTesting public interface AnimationClockTestRule extends org.junit.rules.TestRule {
+  @androidx.compose.ui.test.ExperimentalTestApi public interface AnimationClockTestRule extends org.junit.rules.TestRule {
     method public default void advanceClock(long milliseconds);
     method public androidx.compose.ui.test.TestAnimationClock getClock();
     method public default boolean isPaused();
@@ -52,7 +52,7 @@
   }
 
   public interface ComposeTestRule extends org.junit.rules.TestRule androidx.compose.ui.test.SemanticsNodeInteractionsProvider {
-    method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
     method public androidx.compose.ui.unit.Density getDensity();
     method public long getDisplaySize-YbymL2g();
@@ -67,7 +67,7 @@
     property public abstract long displaySize;
   }
 
-  @androidx.compose.ui.test.InternalTestingApi public final class DisableTransitionsTestRule implements org.junit.rules.TestRule {
+  @androidx.compose.ui.test.InternalTestApi public final class DisableTransitionsTestRule implements org.junit.rules.TestRule {
     ctor public DisableTransitionsTestRule(boolean disableTransitions);
     ctor public DisableTransitionsTestRule();
     method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
@@ -91,9 +91,9 @@
 
   public final class ComposeIdlingResourceKt {
     method @Deprecated public static void registerComposeWithEspresso();
-    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
     method @Deprecated public static void unregisterComposeFromEspresso();
-    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
   }
 
   public final class ComposeNotIdleException extends java.lang.Throwable {
diff --git a/compose/ui/ui-test-junit4/api/restricted_current.txt b/compose/ui/ui-test-junit4/api/restricted_current.txt
index f4d018a..a6f3662 100644
--- a/compose/ui/ui-test-junit4/api/restricted_current.txt
+++ b/compose/ui/ui-test-junit4/api/restricted_current.txt
@@ -2,13 +2,13 @@
 package androidx.compose.ui.test.junit4 {
 
   public final class AndroidAnimationClockTestRuleKt {
-    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
   }
 
   public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
     ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
     method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
-    method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public R getActivityRule();
     method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
     method public androidx.compose.ui.unit.Density getDensity();
@@ -41,7 +41,7 @@
   public final class AndroidSynchronizationKt {
   }
 
-  @androidx.compose.ui.test.ExperimentalTesting public interface AnimationClockTestRule extends org.junit.rules.TestRule {
+  @androidx.compose.ui.test.ExperimentalTestApi public interface AnimationClockTestRule extends org.junit.rules.TestRule {
     method public default void advanceClock(long milliseconds);
     method public androidx.compose.ui.test.TestAnimationClock getClock();
     method public default boolean isPaused();
@@ -52,7 +52,7 @@
   }
 
   public interface ComposeTestRule extends org.junit.rules.TestRule androidx.compose.ui.test.SemanticsNodeInteractionsProvider {
-    method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
     method public androidx.compose.ui.unit.Density getDensity();
     method public long getDisplaySize-YbymL2g();
@@ -67,7 +67,7 @@
     property public abstract long displaySize;
   }
 
-  @androidx.compose.ui.test.InternalTestingApi public final class DisableTransitionsTestRule implements org.junit.rules.TestRule {
+  @androidx.compose.ui.test.InternalTestApi public final class DisableTransitionsTestRule implements org.junit.rules.TestRule {
     ctor public DisableTransitionsTestRule(boolean disableTransitions);
     ctor public DisableTransitionsTestRule();
     method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
@@ -91,9 +91,9 @@
 
   public final class ComposeIdlingResourceKt {
     method @Deprecated public static void registerComposeWithEspresso();
-    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
     method @Deprecated public static void unregisterComposeFromEspresso();
-    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
   }
 
   public final class ComposeNotIdleException extends java.lang.Throwable {
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt
index cde1ff1..7f6c642 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt
@@ -17,7 +17,7 @@
 package androidx.compose.ui.test.junit4
 
 import androidx.compose.ui.test.IdlingResource
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
 import androidx.test.annotation.UiThreadTest
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -40,7 +40,7 @@
     private var onIdleCalled = false
     @OptIn(ExperimentalCoroutinesApi::class)
     private val scope = TestCoroutineScope()
-    @OptIn(InternalTestingApi::class)
+    @OptIn(InternalTestApi::class)
     private val registry = IdlingResourceRegistry(scope).apply {
         setOnIdleCallback { onIdleCalled = true }
     }
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRuleTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRuleTest.kt
index ec13577..0607fb5 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRuleTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRuleTest.kt
@@ -35,7 +35,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.test.espresso.Espresso.onIdle
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
@@ -45,7 +45,7 @@
 import org.junit.Test
 
 @LargeTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class MonotonicFrameClockTestRuleTest {
 
     companion object {
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
index 886546d..d2a6f5a 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
@@ -38,7 +38,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.test.espresso.Espresso.onIdle
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
@@ -48,7 +48,7 @@
 import org.junit.Test
 
 @LargeTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class TestAnimationClockTest {
 
     companion object {
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitingForOnCommitCallbackTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitingForOnCommitCallbackTest.kt
index 644b2b5..65b5d60 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitingForOnCommitCallbackTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitingForOnCommitCallbackTest.kt
@@ -20,7 +20,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.onCommit
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
@@ -125,7 +125,7 @@
     fun cascadingOnCommits_suspendedWait_mainDispatcher() =
         cascadingOnCommits_suspendedWait(Dispatchers.Main)
 
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun cascadingOnCommits_suspendedWait(context: CoroutineContext) = runBlocking(context) {
         // Collect unique values (markers) at each step during the process and
         // at the end verify that they were collected in the right order
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
index c417e42..69d290d 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
@@ -18,7 +18,7 @@
 
 import androidx.compose.animation.core.InternalAnimationApi
 import androidx.compose.animation.core.rootAnimationClockFactory
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.TestAnimationClock
 import androidx.compose.ui.test.junit4.android.AndroidTestAnimationClock
 import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
@@ -31,7 +31,7 @@
  * the ambient animation clock provided at the root of the composition tree with a
  * [TestAnimationClock].
  */
-@ExperimentalTesting
+@ExperimentalTestApi
 internal class AndroidAnimationClockTestRule(
     private val composeIdlingResource: ComposeIdlingResource
 ) : AnimationClockTestRule {
@@ -70,7 +70,7 @@
     level = DeprecationLevel.ERROR,
     replaceWith = ReplaceWith("composeTestRule.clockTestRule")
 )
-@ExperimentalTesting
+@ExperimentalTestApi
 @Suppress("DocumentExceptions")
 actual fun createAnimationClockRule(): AnimationClockTestRule =
     throw UnsupportedOperationException()
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
index 572d8c2..b2e9557 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
@@ -22,9 +22,9 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Recomposer
 import androidx.compose.ui.platform.setContent
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.IdlingResource
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.SemanticsNodeInteractionCollection
@@ -85,7 +85,7 @@
 fun <A : ComponentActivity> createAndroidComposeRule(
     activityClass: Class<A>
 ): AndroidComposeTestRule<ActivityScenarioRule<A>, A> =
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     createAndroidComposeRule(
         activityClass = activityClass,
         driveClockByMonotonicFrameClock = false
@@ -97,14 +97,14 @@
  * experimental and _will_ be removed in the future. See the other overloads of
  * [createAndroidComposeRule] for the recommended way of creating a [ComposeTestRule].
  */
-@ExperimentalTesting
+@ExperimentalTestApi
 internal fun createAndroidComposeRule(
     driveClockByMonotonicFrameClock: Boolean
 ): AndroidComposeTestRule<ActivityScenarioRule<ComponentActivity>, ComponentActivity> {
     return createAndroidComposeRule(ComponentActivity::class.java, driveClockByMonotonicFrameClock)
 }
 
-@ExperimentalTesting
+@ExperimentalTestApi
 private fun <A : ComponentActivity> createAndroidComposeRule(
     activityClass: Class<A>,
     driveClockByMonotonicFrameClock: Boolean
@@ -125,16 +125,16 @@
  * @param activityProvider To resolve the activity from the given test rule. Must be a blocking
  * function.
  */
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
 class AndroidComposeTestRule<R : TestRule, A : ComponentActivity>
-@ExperimentalTesting
+@ExperimentalTestApi
 internal constructor(
     val activityRule: R,
     private val activityProvider: (R) -> A,
     driveClockByMonotonicFrameClock: Boolean
 ) : ComposeTestRule {
 
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     constructor(
         activityRule: R,
         activityProvider: (R) -> A
@@ -148,7 +148,7 @@
         registerIdlingResource(it)
     }
 
-    @ExperimentalTesting
+    @ExperimentalTestApi
     override val clockTestRule: AnimationClockTestRule =
         if (!driveClockByMonotonicFrameClock) {
             AndroidAnimationClockTestRule(composeIdlingResource)
@@ -183,7 +183,7 @@
 
     override fun apply(base: Statement, description: Description): Statement {
         @Suppress("NAME_SHADOWING")
-        @OptIn(ExperimentalTesting::class)
+        @OptIn(ExperimentalTestApi::class)
         return RuleChain
             .outerRule { base, _ -> composeIdlingResource.getStatementFor(base) }
             .around { base, _ -> idlingResourceRegistry.getStatementFor(base) }
@@ -228,7 +228,7 @@
         composeIdlingResource.waitForIdle()
     }
 
-    @ExperimentalTesting
+    @ExperimentalTestApi
     override suspend fun awaitIdle() {
         composeIdlingResource.awaitIdle()
     }
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
index ded27d1..0c4c387 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
@@ -20,13 +20,13 @@
 import androidx.compose.ui.node.Owner
 import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.semantics.SemanticsNode
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
 import androidx.compose.ui.test.TestOwner
 import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
 import androidx.compose.ui.text.input.EditOperation
 import androidx.compose.ui.text.input.ImeAction
 
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
 internal class AndroidTestOwner(
     private val composeIdlingResource: ComposeIdlingResource
 ) : TestOwner {
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/DisableTransitionsTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/DisableTransitionsTestRule.kt
index 13dc642..9048acfc 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/DisableTransitionsTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/DisableTransitionsTestRule.kt
@@ -19,7 +19,7 @@
 import androidx.compose.animation.core.InternalAnimationApi
 import androidx.compose.animation.transition
 import androidx.compose.animation.transitionsEnabled
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
@@ -29,7 +29,7 @@
  * can be turned into a no-op by setting [disableTransitions] to `false`, allowing you to put it
  * into a rule chain without branching logic.
  */
-@InternalTestingApi
+@InternalTestApi
 class DisableTransitionsTestRule(private val disableTransitions: Boolean = false) : TestRule {
 
     override fun apply(base: Statement, description: Description?): Statement {
@@ -58,5 +58,5 @@
     )
 )
 @Suppress("unused")
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
 typealias DisableTransitions = DisableTransitionsTestRule
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
index 8aa2f09..3aae3b8 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
@@ -20,14 +20,14 @@
 import androidx.compose.animation.core.InternalAnimationApi
 import androidx.compose.animation.core.MonotonicFrameAnimationClock
 import androidx.compose.animation.core.rootAnimationClockFactory
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.TestAnimationClock
 import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
 import kotlinx.coroutines.CoroutineScope
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 
-@ExperimentalTesting
+@ExperimentalTestApi
 internal class MonotonicFrameClockTestRule(
     private val composeIdlingResource: ComposeIdlingResource
 ) : AnimationClockTestRule {
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
index 0de743a..b0af5ca 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
@@ -19,7 +19,7 @@
 import android.view.View
 import androidx.annotation.VisibleForTesting
 import androidx.compose.ui.platform.ViewRootForTest
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import kotlinx.coroutines.suspendCancellableCoroutine
 import org.junit.runners.model.Statement
 import java.util.Collections
@@ -203,7 +203,7 @@
     }
 }
 
-@ExperimentalTesting
+@ExperimentalTestApi
 @OptIn(ExperimentalTime::class)
 internal suspend fun AndroidOwnerRegistry.awaitAndroidOwners() {
     ensureAndroidOwnerRegistryIsSetUp()
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidTestAnimationClock.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidTestAnimationClock.kt
index be6439b..db46102 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidTestAnimationClock.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidTestAnimationClock.kt
@@ -19,7 +19,7 @@
 import android.view.Choreographer
 import androidx.compose.animation.core.AnimationClockObserver
 import androidx.compose.animation.core.ManualAnimationClock
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.TestAnimationClock
 import androidx.compose.ui.test.junit4.runOnUiThread
 
@@ -36,7 +36,7 @@
  * @see advanceClock
  * @see resumeClock
  */
-@ExperimentalTesting
+@ExperimentalTestApi
 internal class AndroidTestAnimationClock : TestAnimationClock {
 
     /**
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
index df32ab2..c2def80 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
@@ -21,7 +21,7 @@
 import androidx.compose.runtime.Recomposer
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.node.Owner
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.IdlingResource
 import androidx.compose.ui.test.TestAnimationClock
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
@@ -72,7 +72,7 @@
     level = DeprecationLevel.ERROR,
     replaceWith = ReplaceWith("composeIdlingResource.registerTestClock(clock)")
 )
-@ExperimentalTesting
+@ExperimentalTestApi
 @Suppress("UNUSED_PARAMETER", "DocumentExceptions")
 fun registerTestClock(clock: TestAnimationClock): Unit = throw UnsupportedOperationException(
     "Global (un)registration of TestAnimationClocks is no longer supported. Register clocks " +
@@ -88,7 +88,7 @@
     level = DeprecationLevel.ERROR,
     replaceWith = ReplaceWith("composeIdlingResource.unregisterTestClock(clock)")
 )
-@ExperimentalTesting
+@ExperimentalTestApi
 @Suppress("UNUSED_PARAMETER", "DocumentExceptions")
 fun unregisterTestClock(clock: TestAnimationClock): Unit = throw UnsupportedOperationException(
     "Global (un)registration of TestAnimationClocks is no longer supported. Register clocks " +
@@ -107,7 +107,7 @@
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     internal val androidOwnerRegistry = AndroidOwnerRegistry()
 
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     private val clocks = mutableSetOf<TestAnimationClock>()
 
     private var hadAnimationClocksIdle = true
@@ -142,21 +142,21 @@
                 hadNoPendingDraw*/
         }
 
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun registerTestClock(clock: TestAnimationClock) {
         synchronized(clocks) {
             clocks.add(clock)
         }
     }
 
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     fun unregisterTestClock(clock: TestAnimationClock) {
         synchronized(clocks) {
             clocks.remove(clock)
         }
     }
 
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     private fun areAllClocksIdle(): Boolean {
         return synchronized(clocks) {
             clocks.all { it.isIdle }
@@ -221,7 +221,7 @@
         //  waitForAndroidOwners() suggests that we are now guaranteed one.
     }
 
-    @ExperimentalTesting
+    @ExperimentalTestApi
     suspend fun awaitIdle() {
         // TODO(b/169038516): when we can query AndroidOwners for measure or layout, remove
         //  runEspressoOnIdle() and replace it with a suspend fun that loops while the
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
index 20c2fb6..86422f4 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
@@ -18,12 +18,12 @@
 
 import androidx.compose.animation.core.AnimationClockObserver
 import androidx.compose.animation.core.InternalAnimationApi
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.TestAnimationClock
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 
-@ExperimentalTesting
+@ExperimentalTestApi
 internal class DesktopTestAnimationClock : TestAnimationClock {
     override val isIdle: Boolean
         get() = TODO("Not yet implemented")
@@ -52,7 +52,7 @@
     }
 }
 
-@ExperimentalTesting
+@ExperimentalTestApi
 internal class DesktopAnimationClockTestRule : AnimationClockTestRule {
 
     override val clock: TestAnimationClock get() = DesktopTestAnimationClock()
@@ -95,7 +95,7 @@
     level = DeprecationLevel.ERROR,
     replaceWith = ReplaceWith("composeTestRule.clockTestRule")
 )
-@ExperimentalTesting
+@ExperimentalTestApi
 @Suppress("DocumentExceptions")
 actual fun createAnimationClockRule(): AnimationClockTestRule =
-    throw UnsupportedOperationException()
\ No newline at end of file
+    throw UnsupportedOperationException()
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
index f66b098..706f304 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
@@ -25,9 +25,9 @@
 import androidx.compose.ui.platform.DesktopOwners
 import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.semantics.SemanticsNode
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.IdlingResource
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.SemanticsNodeInteractionCollection
@@ -49,7 +49,7 @@
 
 actual fun createComposeRule(): ComposeTestRule = DesktopComposeTestRule()
 
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
 class DesktopComposeTestRule : ComposeTestRule {
 
     companion object {
@@ -59,7 +59,7 @@
     var owners: DesktopOwners? = null
     private var owner: DesktopOwner? = null
 
-    @ExperimentalTesting
+    @ExperimentalTestApi
     override val clockTestRule: AnimationClockTestRule = DesktopAnimationClockTestRule()
 
     override val density: Density
@@ -104,7 +104,7 @@
         }
     }
 
-    @ExperimentalTesting
+    @ExperimentalTestApi
     override suspend fun awaitIdle() {
         while (!isIdle()) {
             runExecutionQueue()
@@ -159,7 +159,7 @@
     }
 
     private fun performSetContent(composable: @Composable() () -> Unit) {
-        val surface = Surface.makeRasterN32Premul(displaySize.width, displaySize.height)
+        val surface = Surface.makeRasterN32Premul(displaySize.width, displaySize.height)!!
         val canvas = surface.canvas
         val owners = DesktopOwners(invalidate = {}).also {
             owners = it
@@ -203,4 +203,4 @@
             return rule.owners!!.list
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
index 3fbde5c..4a549fe 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
@@ -16,11 +16,11 @@
 
 package androidx.compose.ui.test.junit4
 
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.TestAnimationClock
 import org.junit.rules.TestRule
 
-@ExperimentalTesting
+@ExperimentalTestApi
 interface AnimationClockTestRule : TestRule {
     /**
      * The ambient animation clock that is provided at the root of the composition tree.
@@ -54,5 +54,5 @@
     level = DeprecationLevel.ERROR,
     replaceWith = ReplaceWith("composeTestRule.clockTestRule")
 )
-@ExperimentalTesting
+@ExperimentalTestApi
 expect fun createAnimationClockRule(): AnimationClockTestRule
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
index 1190b77..af3bf47 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
@@ -17,7 +17,7 @@
 package androidx.compose.ui.test.junit4
 
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.IdlingResource
 import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
 import androidx.compose.ui.unit.Density
@@ -46,7 +46,7 @@
     /**
      * A test rule that allows you to control the animation clock
      */
-    @OptIn(ExperimentalTesting::class)
+    @OptIn(ExperimentalTestApi::class)
     val clockTestRule: AnimationClockTestRule
 
     /**
@@ -79,7 +79,7 @@
      * Suspends until compose is idle. Compose is idle if there are no pending compositions, no
      * pending changes that could lead to another composition, and no pending draw calls.
      */
-    @ExperimentalTesting
+    @ExperimentalTestApi
     suspend fun awaitIdle()
 
     /**
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt
index 6ffd62e..677a512 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt
@@ -18,7 +18,7 @@
 
 import androidx.annotation.VisibleForTesting
 import androidx.compose.ui.test.IdlingResource
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
@@ -29,12 +29,12 @@
 
 internal class IdlingResourceRegistry
 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-@InternalTestingApi
+@InternalTestApi
 internal constructor(
     private val pollScopeOverride: CoroutineScope?
 ) : IdlingResource {
     // Publicly facing constructor, that doesn't override the poll scope
-    @OptIn(InternalTestingApi::class)
+    @OptIn(InternalTestApi::class)
     constructor() : this(null)
 
     private val lock = Any()
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index d88b6ad..26826d2 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -24,8 +24,8 @@
   }
 
   public final class AnimationClocksKt {
-    method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
-    method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
   }
 
   public final class AssertionsKt {
@@ -71,13 +71,16 @@
   }
 
   public final class CoroutineBuildersKt {
-    method @androidx.compose.ui.test.ExperimentalTesting public static <R> void runBlockingWithManualClock(optional boolean compatibleWithManualAnimationClock, kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.animation.core.ManualFrameClock,? super kotlin.coroutines.Continuation<? super R>,?> block);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static <R> void runBlockingWithManualClock(optional boolean compatibleWithManualAnimationClock, kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.animation.core.ManualFrameClock,? super kotlin.coroutines.Continuation<? super R>,?> block);
   }
 
   public final class ErrorMessagesKt {
   }
 
-  @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTesting {
+  @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTestApi {
+  }
+
+  @Deprecated @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTesting {
   }
 
   public final class FiltersKt {
@@ -185,7 +188,10 @@
     property public abstract boolean isIdleNow;
   }
 
-  @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
+  @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestApi {
+  }
+
+  @Deprecated @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
   }
 
   public final class KeyInputHelpersKt {
@@ -270,7 +276,7 @@
   public final class SemanticsSelectorKt {
   }
 
-  @androidx.compose.ui.test.ExperimentalTesting public interface TestAnimationClock extends androidx.compose.animation.core.AnimationClockObservable {
+  @androidx.compose.ui.test.ExperimentalTestApi public interface TestAnimationClock extends androidx.compose.animation.core.AnimationClockObservable {
     method public void advanceClock(long milliseconds);
     method public boolean isIdle();
     method public boolean isPaused();
@@ -295,7 +301,7 @@
     method public static long getFrameDelayMillis(androidx.compose.ui.test.TestMonotonicFrameClock);
   }
 
-  @androidx.compose.ui.test.InternalTestingApi public interface TestOwner {
+  @androidx.compose.ui.test.InternalTestApi public interface TestOwner {
     method public java.util.Set<androidx.compose.ui.node.Owner> getOwners();
     method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
     method public void sendImeAction(androidx.compose.ui.semantics.SemanticsNode node, androidx.compose.ui.text.input.ImeAction actionSpecified);
@@ -303,10 +309,10 @@
   }
 
   public final class TestOwnerKt {
-    method @androidx.compose.ui.test.InternalTestingApi public static androidx.compose.ui.test.TestContext createTestContext(androidx.compose.ui.test.TestOwner owner);
+    method @androidx.compose.ui.test.InternalTestApi public static androidx.compose.ui.test.TestContext createTestContext(androidx.compose.ui.test.TestOwner owner);
   }
 
-  @androidx.compose.ui.test.ExperimentalTesting public final class TestUiDispatcher {
+  @androidx.compose.ui.test.ExperimentalTestApi public final class TestUiDispatcher {
     method @Deprecated public kotlin.coroutines.CoroutineContext getMain();
     property @Deprecated public final kotlin.coroutines.CoroutineContext Main;
     field public static final androidx.compose.ui.test.TestUiDispatcher INSTANCE;
diff --git a/compose/ui/ui-test/api/public_plus_experimental_current.txt b/compose/ui/ui-test/api/public_plus_experimental_current.txt
index d88b6ad..26826d2 100644
--- a/compose/ui/ui-test/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test/api/public_plus_experimental_current.txt
@@ -24,8 +24,8 @@
   }
 
   public final class AnimationClocksKt {
-    method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
-    method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
   }
 
   public final class AssertionsKt {
@@ -71,13 +71,16 @@
   }
 
   public final class CoroutineBuildersKt {
-    method @androidx.compose.ui.test.ExperimentalTesting public static <R> void runBlockingWithManualClock(optional boolean compatibleWithManualAnimationClock, kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.animation.core.ManualFrameClock,? super kotlin.coroutines.Continuation<? super R>,?> block);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static <R> void runBlockingWithManualClock(optional boolean compatibleWithManualAnimationClock, kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.animation.core.ManualFrameClock,? super kotlin.coroutines.Continuation<? super R>,?> block);
   }
 
   public final class ErrorMessagesKt {
   }
 
-  @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTesting {
+  @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTestApi {
+  }
+
+  @Deprecated @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTesting {
   }
 
   public final class FiltersKt {
@@ -185,7 +188,10 @@
     property public abstract boolean isIdleNow;
   }
 
-  @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
+  @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestApi {
+  }
+
+  @Deprecated @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
   }
 
   public final class KeyInputHelpersKt {
@@ -270,7 +276,7 @@
   public final class SemanticsSelectorKt {
   }
 
-  @androidx.compose.ui.test.ExperimentalTesting public interface TestAnimationClock extends androidx.compose.animation.core.AnimationClockObservable {
+  @androidx.compose.ui.test.ExperimentalTestApi public interface TestAnimationClock extends androidx.compose.animation.core.AnimationClockObservable {
     method public void advanceClock(long milliseconds);
     method public boolean isIdle();
     method public boolean isPaused();
@@ -295,7 +301,7 @@
     method public static long getFrameDelayMillis(androidx.compose.ui.test.TestMonotonicFrameClock);
   }
 
-  @androidx.compose.ui.test.InternalTestingApi public interface TestOwner {
+  @androidx.compose.ui.test.InternalTestApi public interface TestOwner {
     method public java.util.Set<androidx.compose.ui.node.Owner> getOwners();
     method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
     method public void sendImeAction(androidx.compose.ui.semantics.SemanticsNode node, androidx.compose.ui.text.input.ImeAction actionSpecified);
@@ -303,10 +309,10 @@
   }
 
   public final class TestOwnerKt {
-    method @androidx.compose.ui.test.InternalTestingApi public static androidx.compose.ui.test.TestContext createTestContext(androidx.compose.ui.test.TestOwner owner);
+    method @androidx.compose.ui.test.InternalTestApi public static androidx.compose.ui.test.TestContext createTestContext(androidx.compose.ui.test.TestOwner owner);
   }
 
-  @androidx.compose.ui.test.ExperimentalTesting public final class TestUiDispatcher {
+  @androidx.compose.ui.test.ExperimentalTestApi public final class TestUiDispatcher {
     method @Deprecated public kotlin.coroutines.CoroutineContext getMain();
     property @Deprecated public final kotlin.coroutines.CoroutineContext Main;
     field public static final androidx.compose.ui.test.TestUiDispatcher INSTANCE;
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index d88b6ad..26826d2 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -24,8 +24,8 @@
   }
 
   public final class AnimationClocksKt {
-    method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
-    method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
   }
 
   public final class AssertionsKt {
@@ -71,13 +71,16 @@
   }
 
   public final class CoroutineBuildersKt {
-    method @androidx.compose.ui.test.ExperimentalTesting public static <R> void runBlockingWithManualClock(optional boolean compatibleWithManualAnimationClock, kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.animation.core.ManualFrameClock,? super kotlin.coroutines.Continuation<? super R>,?> block);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static <R> void runBlockingWithManualClock(optional boolean compatibleWithManualAnimationClock, kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.animation.core.ManualFrameClock,? super kotlin.coroutines.Continuation<? super R>,?> block);
   }
 
   public final class ErrorMessagesKt {
   }
 
-  @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTesting {
+  @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTestApi {
+  }
+
+  @Deprecated @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTesting {
   }
 
   public final class FiltersKt {
@@ -185,7 +188,10 @@
     property public abstract boolean isIdleNow;
   }
 
-  @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
+  @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestApi {
+  }
+
+  @Deprecated @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
   }
 
   public final class KeyInputHelpersKt {
@@ -270,7 +276,7 @@
   public final class SemanticsSelectorKt {
   }
 
-  @androidx.compose.ui.test.ExperimentalTesting public interface TestAnimationClock extends androidx.compose.animation.core.AnimationClockObservable {
+  @androidx.compose.ui.test.ExperimentalTestApi public interface TestAnimationClock extends androidx.compose.animation.core.AnimationClockObservable {
     method public void advanceClock(long milliseconds);
     method public boolean isIdle();
     method public boolean isPaused();
@@ -295,7 +301,7 @@
     method public static long getFrameDelayMillis(androidx.compose.ui.test.TestMonotonicFrameClock);
   }
 
-  @androidx.compose.ui.test.InternalTestingApi public interface TestOwner {
+  @androidx.compose.ui.test.InternalTestApi public interface TestOwner {
     method public java.util.Set<androidx.compose.ui.node.Owner> getOwners();
     method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
     method public void sendImeAction(androidx.compose.ui.semantics.SemanticsNode node, androidx.compose.ui.text.input.ImeAction actionSpecified);
@@ -303,10 +309,10 @@
   }
 
   public final class TestOwnerKt {
-    method @androidx.compose.ui.test.InternalTestingApi public static androidx.compose.ui.test.TestContext createTestContext(androidx.compose.ui.test.TestOwner owner);
+    method @androidx.compose.ui.test.InternalTestApi public static androidx.compose.ui.test.TestContext createTestContext(androidx.compose.ui.test.TestOwner owner);
   }
 
-  @androidx.compose.ui.test.ExperimentalTesting public final class TestUiDispatcher {
+  @androidx.compose.ui.test.ExperimentalTestApi public final class TestUiDispatcher {
     method @Deprecated public kotlin.coroutines.CoroutineContext getMain();
     property @Deprecated public final kotlin.coroutines.CoroutineContext Main;
     field public static final androidx.compose.ui.test.TestUiDispatcher INSTANCE;
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/inputdispatcher/InputDispatcherTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/inputdispatcher/InputDispatcherTest.kt
index 7cb1c84..fd5a3dc 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/inputdispatcher/InputDispatcherTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/inputdispatcher/InputDispatcherTest.kt
@@ -19,7 +19,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.test.AndroidInputDispatcher
 import androidx.compose.ui.test.InputDispatcher
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
 import androidx.compose.ui.test.createTestContext
 import androidx.compose.ui.test.util.InputDispatcherTestRule
 import androidx.compose.ui.test.util.MotionEventRecorder
@@ -29,7 +29,7 @@
 import org.junit.Rule
 import org.junit.rules.TestRule
 
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
 open class InputDispatcherTest(eventPeriodOverride: Long? = null) {
 
     @get:Rule
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/CoroutineBuilders.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/CoroutineBuilders.kt
index 4626cb2..63c8a6cf 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/CoroutineBuilders.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/CoroutineBuilders.kt
@@ -72,7 +72,7 @@
  * first frame immediately upon subscription. Avoid reliance on this if possible. `false` by
  * default.
  */
-@ExperimentalTesting
+@ExperimentalTestApi
 fun <R> runBlockingWithManualClock(
     compatibleWithManualAnimationClock: Boolean = false,
     block: suspend CoroutineScope.(clock: ManualFrameClock) -> R
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
index c680bb8..109ea4a 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
@@ -98,7 +98,7 @@
         0f
     }
 
-    @OptIn(InternalTestingApi::class)
+    @OptIn(InternalTestApi::class)
     testContext.testOwner.runOnUiThread {
         scrollableNode.config[SemanticsActions.ScrollBy].action(dx, dy)
     }
@@ -182,7 +182,7 @@
     }
 
     @Suppress("DEPRECATION")
-    @OptIn(InternalTestingApi::class)
+    @OptIn(InternalTestApi::class)
     testContext.testOwner.runOnUiThread {
         invocation(node.config[key].action)
     }
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/AnimationClocks.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/AnimationClocks.kt
index d2b0062..d883a9e2 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/AnimationClocks.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/AnimationClocks.kt
@@ -32,7 +32,7 @@
  * Use [pauseClock] to switch from automatic ticking to manual ticking, [resumeClock] to switch
  * from manual to automatic with; and manually tick the clock with [advanceClock].
  */
-@ExperimentalTesting
+@ExperimentalTestApi
 interface TestAnimationClock : AnimationClockObservable {
     /**
      * Whether the clock is idle or not. An idle clock is one that is not driving animations,
@@ -73,7 +73,7 @@
  *
  * @see MonotonicFrameAnimationClock
  */
-@ExperimentalTesting
+@ExperimentalTestApi
 fun monotonicFrameAnimationClockOf(
     coroutineContext: CoroutineContext,
     clock: MonotonicFrameClock
@@ -88,7 +88,7 @@
  *
  * @see MonotonicFrameAnimationClock
  */
-@ExperimentalTesting
+@ExperimentalTestApi
 fun monotonicFrameAnimationClockOf(
     coroutineContext: CoroutineContext
 ): MonotonicFrameAnimationClock =
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt
new file mode 100644
index 0000000..c4a2c1c
--- /dev/null
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2020 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.compose.ui.test
+
+@Deprecated(
+    "Renamed to ExperimentalTestApi for consistency with other experimental module APIs",
+    replaceWith = ReplaceWith("ExperimentalTestApi", "androidx.compose.ui.test.ExperimentalTestApi")
+)
+@RequiresOptIn("This testing API is experimental and is likely to be changed or removed entirely")
+annotation class ExperimentalTesting
+
+@RequiresOptIn("This testing API is experimental and is likely to be changed or removed entirely")
+annotation class ExperimentalTestApi
+
+@Deprecated(
+    "Renamed to InternalTestApi for consistency with other internal module APIs",
+    replaceWith = ReplaceWith("InternalTestApi", "androidx.compose.ui.test.InternalTestApi")
+)
+@RequiresOptIn(
+    "This is internal API for Compose modules that may change frequently and without warning."
+)
+annotation class InternalTestingApi
+
+@RequiresOptIn(
+    "This is internal API for Compose modules that may change frequently and without warning."
+)
+annotation class InternalTestApi
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
index 86871e9..0af8010 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
@@ -27,6 +27,6 @@
     val semanticsNode = fetchSemanticsNode("Failed to send key Event (${keyEvent.key})")
     val owner = semanticsNode.owner
     requireNotNull(owner) { "Failed to find owner" }
-    @OptIn(InternalTestingApi::class)
+    @OptIn(InternalTestApi::class)
     return testContext.testOwner.runOnUiThread { owner.sendKeyEvent(keyEvent) }
 }
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
index 9624c69..8c17354 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
@@ -27,7 +27,7 @@
  *
  * This is typically implemented by entities like test rule.
  */
-@InternalTestingApi
+@InternalTestApi
 interface TestOwner {
 
     /**
@@ -67,17 +67,17 @@
  * Can crash in case it hits time out. This is not supposed to be handled as it
  * surfaces only in incorrect tests.
  */
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
 internal fun TestOwner.getAllSemanticsNodes(useUnmergedTree: Boolean): List<SemanticsNode> {
     return getOwners().flatMap { it.semanticsOwner.getAllSemanticsNodes(useUnmergedTree) }
 }
 
-@InternalTestingApi
+@InternalTestApi
 fun createTestContext(owner: TestOwner): TestContext {
     return TestContext(owner)
 }
 
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
 class TestContext internal constructor(internal val testOwner: TestOwner) {
 
     /**
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestUiDispatcher.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestUiDispatcher.kt
index 2c0c3cd..e6a9d34 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestUiDispatcher.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestUiDispatcher.kt
@@ -19,7 +19,7 @@
 import kotlinx.coroutines.Dispatchers
 import kotlin.coroutines.CoroutineContext
 
-@ExperimentalTesting
+@ExperimentalTestApi
 object TestUiDispatcher {
     /**
      * The dispatcher to use if you need to dispatch coroutines on the main thread in tests.
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TextActions.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TextActions.kt
index e12bc35..65062b3 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TextActions.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TextActions.kt
@@ -110,7 +110,7 @@
         )
     }
 
-    @OptIn(InternalTestingApi::class)
+    @OptIn(InternalTestApi::class)
     testContext.testOwner.sendImeAction(node, actionSpecified)
 }
 
@@ -119,6 +119,6 @@
     val node = fetchSemanticsNode(errorOnFail)
     assert(hasSetTextAction()) { errorOnFail }
 
-    @OptIn(InternalTestingApi::class)
+    @OptIn(InternalTestApi::class)
     testContext.testOwner.sendTextInputCommand(node, command)
 }
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
index 3c47455..1f05602 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
+++ b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
@@ -32,7 +32,7 @@
     val density: Density = Density(1f, 1f),
     var desktopPlatform: DesktopPlatform = DesktopPlatform.Linux
 ) {
-    val surface = Surface.makeRasterN32Premul(width, height)
+    val surface = Surface.makeRasterN32Premul(width, height)!!
     val canvas = surface.canvas
     val owners = DesktopOwners(invalidate = {})
 
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
index cd34bb7..2f06ec2 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
@@ -19,6 +19,7 @@
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.window.MenuBar
 import java.awt.image.BufferedImage
+import javax.swing.JFrame
 
 /**
  * AppFrame is an abstract class that represents a window.
@@ -121,6 +122,54 @@
     abstract fun removeMenuBar()
 
     /**
+     * Switches the window to fullscreen mode if the window is resizable. If the window is in
+     * fullscreen mode [minimize] and [maximize] methods are ignored.
+     */
+    abstract fun makeFullscreen()
+
+    /**
+     * Returns true if the window is in fullscreen state, false otherwise.
+     */
+    abstract val isFullscreen: Boolean
+        get
+
+    /**
+     * Minimizes the window to the taskbar.
+     */
+    abstract fun minimize()
+
+    /**
+     * Returns true if the window is minimized, false otherwise.
+     */
+    val isMinimized: Boolean
+        get() = window.extendedState == JFrame.ICONIFIED
+
+    /**
+     * Maximizes the window to fill all available screen space.
+     */
+    abstract fun maximize()
+
+    /**
+     * Returns true if the window is maximized, false otherwise.
+     */
+    val isMaximized: Boolean
+        get() = window.extendedState == JFrame.MAXIMIZED_BOTH
+
+    /**
+     * Restores the previous state and size of the window after
+     * maximizing/minimizing/fullscreen mode.
+     */
+    abstract fun restore()
+
+    /**
+     * Sets the ability to resize the window. True - the window can be resized,
+     * false - the window cannot be resized. If the window is in fullscreen mode
+     * setter of this property is ignored. If this property is true the [makeFullscreen()]
+     * method is ignored.
+     */
+    abstract var resizable: Boolean
+
+    /**
      * Sets the new position of the window on the screen.
      *
      * @param x the new x-coordinate of the window.
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
index a4a323b..36b80cd 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
@@ -32,6 +32,7 @@
 import java.awt.event.WindowAdapter
 import java.awt.event.WindowEvent
 import java.awt.image.BufferedImage
+import javax.swing.JFrame
 import javax.swing.JMenuBar
 import javax.swing.SwingUtilities
 import javax.swing.WindowConstants
@@ -52,6 +53,8 @@
  * @param menuBar Window menu bar. The menu bar can be displayed inside a window (Windows,
  * Linux) or at the top of the screen (Mac OS).
  * @param undecorated Removes the native window border if set to true. The default value is false.
+ * @param resizable Makes the window resizable if is set to true and unresizable if is set to
+ * false. The default value is true.
  * @param events Allows to describe events of the window.
  * Supported events: onOpen, onClose, onMinimize, onMaximize, onRestore, onFocusGet, onFocusLost,
  * onResize, onRelocate.
@@ -65,6 +68,7 @@
     icon: BufferedImage? = null,
     menuBar: MenuBar? = null,
     undecorated: Boolean = false,
+    resizable: Boolean = true,
     events: WindowEvents = WindowEvents(),
     onDismissRequest: (() -> Unit)? = null,
     content: @Composable () -> Unit = emptyContent()
@@ -77,6 +81,7 @@
         icon = icon,
         menuBar = menuBar,
         undecorated = undecorated,
+        resizable = resizable,
         events = events,
         onDismissRequest = onDismissRequest
     ).show {
@@ -121,7 +126,10 @@
             })
             addWindowFocusListener(object : WindowAdapter() {
                 override fun windowGainedFocus(event: WindowEvent) {
-                    window.setJMenuBar(parent.menuBar?.menuBar)
+                    // Dialogs should not receive a common application menu bar
+                    if (invoker == null) {
+                        window.setJMenuBar(parent.menuBar?.menuBar)
+                    }
                     events.invokeOnFocusGet()
                 }
                 override fun windowLostFocus(event: WindowEvent) {
@@ -158,6 +166,7 @@
         icon: BufferedImage? = null,
         menuBar: MenuBar? = null,
         undecorated: Boolean = false,
+        resizable: Boolean = true,
         events: WindowEvents = WindowEvents(),
         onDismissRequest: (() -> Unit)? = null
     ) : this(
@@ -168,6 +177,7 @@
         icon = icon,
         menuBar = menuBar,
         undecorated = undecorated,
+        resizable = resizable,
         events = events,
         onDismissRequest = onDismissRequest
     ) {
@@ -188,6 +198,8 @@
      * @param menuBar Window menu bar. The menu bar can be displayed inside a window (Windows,
      * Linux) or at the top of the screen (Mac OS).
      * @param undecorated Removes the native window border if set to true. The default value is false.
+     * @param resizable Makes the window resizable if is set to true and unresizable if is set to
+     * false. The default value is true.
      * @param events Allows to describe events of the window.
      * Supported events: onOpen, onClose, onMinimize, onMaximize, onRestore, onFocusGet, onFocusLost,
      * onResize, onRelocate.
@@ -201,6 +213,7 @@
         icon: BufferedImage? = null,
         menuBar: MenuBar? = null,
         undecorated: Boolean = false,
+        resizable: Boolean = true,
         events: WindowEvents = WindowEvents(),
         onDismissRequest: (() -> Unit)? = null
     ) {
@@ -209,6 +222,7 @@
         setTitle(title)
         setIcon(icon)
         setSize(size.width, size.height)
+        this.resizable = resizable
         if (centered) {
             setWindowCentered()
         } else {
@@ -282,6 +296,72 @@
     }
 
     /**
+     * Returns true if the window is in fullscreen mode, false otherwise.
+     */
+    override val isFullscreen: Boolean
+        get() = window.layer.wrapped.fullscreen
+
+    /**
+     * Switches the window to fullscreen mode if the window is resizable. If the window is in
+     * fullscreen mode [minimize] and [maximize] methods are ignored.
+     */
+    override fun makeFullscreen() {
+        if (!isFullscreen && resizable) {
+            window.layer.wrapped.fullscreen = true
+        }
+    }
+
+    /**
+     * Minimizes the window to the taskbar. If the window is in fullscreen mode this method
+     * is ignored.
+     */
+    override fun minimize() {
+        if (!isFullscreen) {
+            window.setExtendedState(JFrame.ICONIFIED)
+        }
+    }
+
+    /**
+     * Maximizes the window to fill all available screen space. If the window is in fullscreen mode
+     * this method is ignored.
+     */
+    override fun maximize() {
+        if (!isFullscreen) {
+            window.setExtendedState(JFrame.MAXIMIZED_BOTH)
+        }
+    }
+
+    /**
+     * Restores the previous state and size of the window after
+     * maximizing/minimizing/fullscreen mode.
+     */
+    override fun restore() {
+        if (isFullscreen) {
+            window.layer.wrapped.fullscreen = false
+        }
+        window.setExtendedState(JFrame.NORMAL)
+    }
+
+    private var _resizable: Boolean = true
+
+    /**
+     * Sets the ability to resize the window. True - the window can be resized,
+     * false - the window cannot be resized. If the window is in fullscreen mode
+     * setter of this property is ignored. If this property is true the [makeFullscreen()]
+     * method is ignored.
+     */
+    override var resizable: Boolean
+        get() {
+            return window.isResizable()
+        }
+        set(value) {
+            if (!isFullscreen) {
+                _resizable = value
+                window.setResizable(value)
+            }
+        }
+
+    /**
      * Sets the new size of the window.
      *
      * @param width the new width of the window.
@@ -384,11 +464,11 @@
         window.apply {
             defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
             setFocusableWindowState(true)
-            setResizable(true)
             setEnabled(true)
             toFront()
             requestFocus()
         }
+        resizable = _resizable
         disconnectPair()
     }
 
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
index 66110a8..062c2a9 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
@@ -50,6 +50,7 @@
 
 internal class ComposeLayer {
 
+    private var composition: Composition? = null
     private val events = AWTDebounceEventQueue()
 
     var owners: DesktopOwners? = null
@@ -275,6 +276,7 @@
     }
 
     fun dispose() {
+        composition?.dispose()
         events.cancel()
         check(!isDisposed)
         frameDispatcher.cancel()
@@ -298,7 +300,7 @@
         invalidate: () -> Unit = this::needRedrawLayer,
         parentComposition: CompositionReference? = null,
         content: @Composable () -> Unit
-    ): Composition {
+    ) {
         check(owners == null) {
             "Cannot setContent twice."
         }
@@ -306,7 +308,7 @@
         val desktopOwner = DesktopOwner(desktopOwners, density)
 
         owners = desktopOwners
-        val composition = desktopOwner.setContent(parent = parentComposition, content = content)
+        composition = desktopOwner.setContent(parent = parentComposition, content = content)
 
         onDensityChanged(desktopOwner::density::set)
 
@@ -314,7 +316,5 @@
             is AppFrame -> parent.onDispose = desktopOwner::dispose
             is ComposePanel -> parent.onDispose = desktopOwner::dispose
         }
-
-        return composition
     }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
index 6915ec41..57d1f10 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
@@ -114,6 +114,8 @@
     }
 
     override fun paint(g: Graphics?) {
+        super.paint(g)
+        layer?.reinit()
         needRedrawLayer()
     }
 }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
index 4b5c6b7..677caf8 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
@@ -16,7 +16,6 @@
 package androidx.compose.desktop
 
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Composition
 import androidx.compose.runtime.CompositionReference
 import java.awt.event.ComponentAdapter
 import java.awt.event.ComponentEvent
@@ -47,14 +46,12 @@
      *        scheduling of composition updates.
      *        If null then default root composition will be used.
      * @param content Composable content of the ComposeWindow.
-     *
-     * @return Composition of the content.
      */
     fun setContent(
         parentComposition: CompositionReference? = null,
         content: @Composable () -> Unit
-    ): Composition {
-        return layer.setContent(
+    ) {
+        layer.setContent(
             parent = parent,
             invalidate = this::needRedrawLayer,
             parentComposition = parentComposition,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopDialog.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopDialog.kt
index 9b5cc81..f9a249d 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopDialog.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopDialog.kt
@@ -43,6 +43,8 @@
  * @param menuBar Window menu bar. The menu bar can be displayed inside a window (Windows,
  * Linux) or at the top of the screen (Mac OS).
  * @param undecorated Removes the native window border if set to true. The default value is false.
+ * @param resizable Makes the window resizable if is set to true and unresizable if is set to
+ * false. The default value is true.
  * @param events Allows to describe events of the window.
  * Supported events: onOpen, onClose, onMinimize, onMaximize, onRestore, onFocusGet, onFocusLost,
  * onResize, onRelocate.
@@ -56,6 +58,7 @@
     val icon: BufferedImage? = null,
     val menuBar: MenuBar? = null,
     val undecorated: Boolean = false,
+    val resizable: Boolean = true,
     val events: WindowEvents = WindowEvents()
 ) : DialogProperties
 
@@ -86,6 +89,7 @@
             icon = desktopProperties.icon,
             menuBar = desktopProperties.menuBar,
             undecorated = desktopProperties.undecorated,
+            resizable = desktopProperties.resizable,
             events = desktopProperties.events,
             onDismissRequest = onDismissRequest
         )
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
index 6c3c4b2..9f966fa 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
@@ -55,7 +55,7 @@
         nanoTime = { currentTimeMillis * 1_000_000 }
     )
 
-    val surface: Surface = Surface.makeRasterN32Premul(width, height)
+    val surface: Surface = Surface.makeRasterN32Premul(width, height)!!
     val canvas: Canvas = surface.canvas
     val owners = DesktopOwners(
         invalidate = frameDispatcher::scheduleFrame
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt
index 307fc91..b0c68fe 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt
@@ -23,14 +23,14 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.staticAmbientOf
 import androidx.compose.ui.platform.AmbientDensity
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.Density
 import com.google.common.truth.Truth
 import org.junit.Rule
 import org.junit.Test
 
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
 class DesktopPopupTest {
     @get:Rule
     val rule = createComposeRule()
@@ -92,4 +92,4 @@
         rule.waitForIdle()
         Truth.assertThat(densityInsidePopup).isEqualTo(3f)
     }
-}
\ No newline at end of file
+}
diff --git a/datastore/datastore-core/api/current.txt b/datastore/datastore-core/api/current.txt
index 6d4f585..d0bb6a8 100644
--- a/datastore/datastore-core/api/current.txt
+++ b/datastore/datastore-core/api/current.txt
@@ -5,10 +5,6 @@
     ctor public CorruptionException(String message, Throwable? cause);
   }
 
-  public interface CorruptionHandler<T> {
-    method public suspend Object? handleCorruption(androidx.datastore.core.CorruptionException ex, kotlin.coroutines.Continuation<? super T> p);
-  }
-
   public interface DataMigration<T> {
     method public suspend Object? cleanUp(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public suspend Object? migrate(T? currentData, kotlin.coroutines.Continuation<? super T> p);
@@ -40,7 +36,7 @@
 
 package androidx.datastore.core.handlers {
 
-  public final class ReplaceFileCorruptionHandler<T> implements androidx.datastore.core.CorruptionHandler<T> {
+  public final class ReplaceFileCorruptionHandler<T> {
     ctor public ReplaceFileCorruptionHandler(kotlin.jvm.functions.Function1<? super androidx.datastore.core.CorruptionException,? extends T> produceNewData);
     method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public suspend Object? handleCorruption(androidx.datastore.core.CorruptionException ex, kotlin.coroutines.Continuation<? super T> p) throws java.io.IOException;
   }
diff --git a/datastore/datastore-core/api/public_plus_experimental_current.txt b/datastore/datastore-core/api/public_plus_experimental_current.txt
index 6d4f585..d0bb6a8 100644
--- a/datastore/datastore-core/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-core/api/public_plus_experimental_current.txt
@@ -5,10 +5,6 @@
     ctor public CorruptionException(String message, Throwable? cause);
   }
 
-  public interface CorruptionHandler<T> {
-    method public suspend Object? handleCorruption(androidx.datastore.core.CorruptionException ex, kotlin.coroutines.Continuation<? super T> p);
-  }
-
   public interface DataMigration<T> {
     method public suspend Object? cleanUp(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public suspend Object? migrate(T? currentData, kotlin.coroutines.Continuation<? super T> p);
@@ -40,7 +36,7 @@
 
 package androidx.datastore.core.handlers {
 
-  public final class ReplaceFileCorruptionHandler<T> implements androidx.datastore.core.CorruptionHandler<T> {
+  public final class ReplaceFileCorruptionHandler<T> {
     ctor public ReplaceFileCorruptionHandler(kotlin.jvm.functions.Function1<? super androidx.datastore.core.CorruptionException,? extends T> produceNewData);
     method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public suspend Object? handleCorruption(androidx.datastore.core.CorruptionException ex, kotlin.coroutines.Continuation<? super T> p) throws java.io.IOException;
   }
diff --git a/datastore/datastore-core/api/restricted_current.txt b/datastore/datastore-core/api/restricted_current.txt
index 6d4f585..d0bb6a8 100644
--- a/datastore/datastore-core/api/restricted_current.txt
+++ b/datastore/datastore-core/api/restricted_current.txt
@@ -5,10 +5,6 @@
     ctor public CorruptionException(String message, Throwable? cause);
   }
 
-  public interface CorruptionHandler<T> {
-    method public suspend Object? handleCorruption(androidx.datastore.core.CorruptionException ex, kotlin.coroutines.Continuation<? super T> p);
-  }
-
   public interface DataMigration<T> {
     method public suspend Object? cleanUp(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public suspend Object? migrate(T? currentData, kotlin.coroutines.Continuation<? super T> p);
@@ -40,7 +36,7 @@
 
 package androidx.datastore.core.handlers {
 
-  public final class ReplaceFileCorruptionHandler<T> implements androidx.datastore.core.CorruptionHandler<T> {
+  public final class ReplaceFileCorruptionHandler<T> {
     ctor public ReplaceFileCorruptionHandler(kotlin.jvm.functions.Function1<? super androidx.datastore.core.CorruptionException,? extends T> produceNewData);
     method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public suspend Object? handleCorruption(androidx.datastore.core.CorruptionException ex, kotlin.coroutines.Continuation<? super T> p) throws java.io.IOException;
   }
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/core/CorruptionHandler.kt b/datastore/datastore-core/src/main/java/androidx/datastore/core/CorruptionHandler.kt
index 5a04a5d..0ce09b6 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/core/CorruptionHandler.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/core/CorruptionHandler.kt
@@ -20,7 +20,7 @@
  * CorruptionHandlers allow recovery from corruption that prevents reading data from the file (as
  * indicated by a CorruptionException).
  */
-public interface CorruptionHandler<T> {
+internal interface CorruptionHandler<T> {
     /**
      * This function will be called by DataStore when it encounters corruption. If the
      * implementation of this function throws an exception, it will be propagated to the original
diff --git a/datastore/datastore-preferences-rxjava2/api/current.txt b/datastore/datastore-preferences-rxjava2/api/current.txt
new file mode 100644
index 0000000..8235e24
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/api/current.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.datastore.preferences.rxjava2 {
+
+  public final class RxPreferenceDataStoreBuilder {
+    ctor public RxPreferenceDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile);
+    ctor public RxPreferenceDataStoreBuilder(android.content.Context context, String name);
+    method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder addDataMigration(androidx.datastore.core.DataMigration<androidx.datastore.preferences.core.Preferences> dataMigration);
+    method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder addRxDataMigration(androidx.datastore.rxjava2.RxDataMigration<androidx.datastore.preferences.core.Preferences> rxDataMigration);
+    method public androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences> build();
+    method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.core.Preferences> corruptionHandler);
+    method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder setIoScheduler(io.reactivex.Scheduler ioScheduler);
+  }
+
+}
+
diff --git a/datastore/datastore-preferences-rxjava2/api/public_plus_experimental_current.txt b/datastore/datastore-preferences-rxjava2/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..8235e24
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/api/public_plus_experimental_current.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.datastore.preferences.rxjava2 {
+
+  public final class RxPreferenceDataStoreBuilder {
+    ctor public RxPreferenceDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile);
+    ctor public RxPreferenceDataStoreBuilder(android.content.Context context, String name);
+    method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder addDataMigration(androidx.datastore.core.DataMigration<androidx.datastore.preferences.core.Preferences> dataMigration);
+    method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder addRxDataMigration(androidx.datastore.rxjava2.RxDataMigration<androidx.datastore.preferences.core.Preferences> rxDataMigration);
+    method public androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences> build();
+    method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.core.Preferences> corruptionHandler);
+    method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder setIoScheduler(io.reactivex.Scheduler ioScheduler);
+  }
+
+}
+
diff --git a/datastore/datastore-preferences-rxjava2/api/res-current.txt b/datastore/datastore-preferences-rxjava2/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/api/res-current.txt
diff --git a/datastore/datastore-preferences-rxjava2/api/restricted_current.txt b/datastore/datastore-preferences-rxjava2/api/restricted_current.txt
new file mode 100644
index 0000000..8235e24
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/api/restricted_current.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.datastore.preferences.rxjava2 {
+
+  public final class RxPreferenceDataStoreBuilder {
+    ctor public RxPreferenceDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile);
+    ctor public RxPreferenceDataStoreBuilder(android.content.Context context, String name);
+    method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder addDataMigration(androidx.datastore.core.DataMigration<androidx.datastore.preferences.core.Preferences> dataMigration);
+    method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder addRxDataMigration(androidx.datastore.rxjava2.RxDataMigration<androidx.datastore.preferences.core.Preferences> rxDataMigration);
+    method public androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences> build();
+    method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.core.Preferences> corruptionHandler);
+    method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder setIoScheduler(io.reactivex.Scheduler ioScheduler);
+  }
+
+}
+
diff --git a/datastore/datastore-preferences-rxjava2/build.gradle b/datastore/datastore-preferences-rxjava2/build.gradle
new file mode 100644
index 0000000..9782a40
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/build.gradle
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2020 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 static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.AndroidXExtension
+import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("kotlin-android")
+}
+
+android {
+    sourceSets {
+        test.java.srcDirs += 'src/test-common/java'
+        androidTest.java.srcDirs += 'src/test-common/java'
+    }
+}
+
+dependencies {
+    api(KOTLIN_STDLIB)
+    api(KOTLIN_COROUTINES_CORE)
+    api("androidx.annotation:annotation:1.1.0")
+    api(RX_JAVA)
+
+    api(project(":datastore:datastore"))
+    api(project(":datastore:datastore-rxjava2"))
+    api(project(":datastore:datastore-preferences"))
+
+    implementation(KOTLIN_COROUTINES_RX2)
+
+    testImplementation(JUNIT)
+    testImplementation(KOTLIN_COROUTINES_TEST)
+    testImplementation(TRUTH)
+    testImplementation(project(":internal-testutils-truth"))
+
+    androidTestImplementation(project(":datastore:datastore-core"))
+    androidTestImplementation(project(":datastore:datastore"))
+    androidTestImplementation(JUNIT)
+    androidTestImplementation(project(":internal-testutils-truth"))
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    androidTestImplementation(ANDROIDX_TEST_CORE)
+}
+
+androidx {
+    name = "Android DataStore Core RxJava2 Wrappers"
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    mavenGroup = LibraryGroups.DATASTORE
+    inceptionYear = "2020"
+    description = "Android DataStore Core - contains wrappers for using DataStore using RxJava2"
+    legacyDisableKotlinStrictApiMode = true
+}
diff --git a/datastore/datastore-preferences-rxjava2/src/androidTest/java/androidx/datastore/preferences/rxjava2/RxPreferencesDataStoreBuilderTest.java b/datastore/datastore-preferences-rxjava2/src/androidTest/java/androidx/datastore/preferences/rxjava2/RxPreferencesDataStoreBuilderTest.java
new file mode 100644
index 0000000..ba1ae8f
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/src/androidTest/java/androidx/datastore/preferences/rxjava2/RxPreferencesDataStoreBuilderTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2020 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.datastore.preferences.rxjava2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.datastore.core.DataStore;
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler;
+import androidx.datastore.preferences.core.MutablePreferences;
+import androidx.datastore.preferences.core.Preferences;
+import androidx.datastore.preferences.core.PreferencesFactory;
+import androidx.datastore.preferences.core.PreferencesKeys;
+import androidx.datastore.rxjava2.RxDataMigration;
+import androidx.datastore.rxjava2.RxDataStore;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+import io.reactivex.Completable;
+import io.reactivex.Single;
+
+public class RxPreferencesDataStoreBuilderTest {
+    @Rule
+    public TemporaryFolder tempFolder = new TemporaryFolder();
+
+    private static final Preferences.Key<Integer> INTEGER_KEY =
+            PreferencesKeys.intKey("int_key");
+
+    private static Single<Preferences> incrementInteger(Preferences preferencesIn) {
+        MutablePreferences prefs = preferencesIn.toMutablePreferences();
+        Integer currentInt = prefs.get(INTEGER_KEY);
+        prefs.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1);
+        return Single.just(prefs);
+    }
+
+    @Test
+    public void testConstructWithProduceFile() throws Exception {
+        File file = tempFolder.newFile("temp.preferences_pb");
+
+        DataStore<Preferences> dataStore =
+                new RxPreferenceDataStoreBuilder(() -> file).build();
+
+        Single<Preferences> incrementInt = RxDataStore.updateDataAsync(dataStore,
+                RxPreferencesDataStoreBuilderTest::incrementInteger);
+        assertThat(incrementInt.blockingGet().get(INTEGER_KEY)).isEqualTo(1);
+
+        // Construct it again and confirm that the data is still there:
+        dataStore = new RxPreferenceDataStoreBuilder(() -> file).build();
+
+        assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY))
+                .isEqualTo(1);
+    }
+
+
+    @Test
+    public void testConstructWithContextAndName() throws Exception {
+
+        Context context = ApplicationProvider.getApplicationContext();
+        String name = "my_data_store";
+
+        File prefsFile = new File(context.getFilesDir().getPath()
+                + "/datastore/" + name + ".preferences_pb");
+        if (prefsFile.exists()) {
+            prefsFile.delete();
+        }
+
+        DataStore<Preferences> dataStore =
+                new RxPreferenceDataStoreBuilder(context, name).build();
+
+        Single<Preferences> set1 = RxDataStore.updateDataAsync(dataStore,
+                RxPreferencesDataStoreBuilderTest::incrementInteger);
+        assertThat(set1.blockingGet().get(INTEGER_KEY)).isEqualTo(1);
+
+        // Construct it again and confirm that the data is still there:
+        dataStore = new RxPreferenceDataStoreBuilder(context, name).build();
+        assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY)).isEqualTo(1);
+
+        // Construct it again with the expected file path and confirm that the data is there:
+        dataStore =
+                new RxPreferenceDataStoreBuilder(
+                        () ->
+                                new File(context.getFilesDir().getPath()
+                                        + "/datastore/" + name + ".preferences_pb")
+                ).build();
+
+        assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY)).isEqualTo(1);
+    }
+
+    @Test
+    public void testMigrationsAreInstalledAndRun() throws Exception {
+        RxDataMigration<Preferences> plusOneMigration = new RxDataMigration<Preferences>() {
+            @NonNull
+            @Override
+            public Single<Boolean> shouldMigrate(@NonNull Preferences currentData) {
+                return Single.just(true);
+            }
+
+            @NonNull
+            @Override
+            public Single<Preferences> migrate(@NonNull Preferences currentData) {
+                return incrementInteger(currentData);
+            }
+
+            @NonNull
+            @Override
+            public Completable cleanUp() {
+                return Completable.complete();
+            }
+        };
+
+        DataStore<Preferences> dataStore =
+                new RxPreferenceDataStoreBuilder(() ->
+                        tempFolder.newFile("temp.preferences_pb"))
+                        .addRxDataMigration(plusOneMigration)
+                        .build();
+
+        assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY))
+                .isEqualTo(1);
+    }
+
+
+    @Test
+    public void testCorruptionHandlerIsUser() throws Exception {
+
+        File file = tempFolder.newFile("temp.preferences_pb");
+
+        try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
+            fileOutputStream.write(0); // will cause corruption exception
+        }
+
+        ReplaceFileCorruptionHandler<Preferences> replaceFileCorruptionHandler =
+                new ReplaceFileCorruptionHandler<Preferences>(exception -> {
+                    MutablePreferences mutablePreferences =
+                            PreferencesFactory.createMutable();
+                    mutablePreferences.set(INTEGER_KEY, 99);
+                    return (Preferences) mutablePreferences;
+                });
+
+
+        DataStore<Preferences> dataStore =
+                new RxPreferenceDataStoreBuilder(() -> file)
+                        .setCorruptionHandler(replaceFileCorruptionHandler)
+                        .build();
+
+        assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY))
+                .isEqualTo(99);
+    }
+}
diff --git a/datastore/datastore-preferences-rxjava2/src/main/AndroidManifest.xml b/datastore/datastore-preferences-rxjava2/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a3c8250
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2020 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.datastore.preferences.rxjava2">
+
+</manifest>
diff --git a/datastore/datastore-preferences-rxjava2/src/main/java/androidx/datastore/preferences/rxjava2/RxPreferenceDataStoreBuilder.kt b/datastore/datastore-preferences-rxjava2/src/main/java/androidx/datastore/preferences/rxjava2/RxPreferenceDataStoreBuilder.kt
new file mode 100644
index 0000000..2075576
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/src/main/java/androidx/datastore/preferences/rxjava2/RxPreferenceDataStoreBuilder.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2020 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.datastore.preferences.rxjava2
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.datastore.core.DataMigration
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.createDataStore
+import androidx.datastore.rxjava2.RxDataMigration
+import io.reactivex.Scheduler
+import io.reactivex.schedulers.Schedulers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.rx2.asCoroutineDispatcher
+import kotlinx.coroutines.rx2.await
+import java.io.File
+import java.util.concurrent.Callable
+
+/**
+ * RxSharedPreferencesMigrationBuilder class for a DataStore that works on a single process.
+ */
+@SuppressLint("TopLevelBuilder")
+public class RxPreferenceDataStoreBuilder {
+
+    // Either produceFile or context & name must be set, but not both.
+    private var produceFile: Callable<File>? = null
+
+    private var context: Context? = null
+    private var name: String? = null
+
+    // Optional
+    private var ioScheduler: Scheduler = Schedulers.io()
+    private var corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null
+    private val dataMigrations: MutableList<DataMigration<Preferences>> = mutableListOf()
+
+    /**
+     * Create a RxPreferenceDataStoreBuilder with the callable which returns the File that
+     * DataStore acts on. The user is responsible for ensuring that there is never more than one
+     * DataStore acting on a file at a time.
+     *
+     * @param produceFile Function which returns the file that the new DataStore will act on. The
+     * function must return the same path every time. No two instances of DataStore should act on
+     * the same file at the same time.
+     */
+    public constructor(produceFile: Callable<File>) {
+        this.produceFile = produceFile
+    }
+
+    /**
+     * Create a RxPreferenceDataStoreBuilder with the Context and name from which to derive the
+     * DataStore file. The file is generated by See [Context.createDataStore] for more info. The
+     * user is responsible for ensuring that there is never more than one DataStore acting on a
+     * file at a time.
+     *
+     * @param context the context from which we retrieve files directory.
+     * @param name the filename relative to Context.filesDir that DataStore acts on. The File is
+     * obtained by calling File(this.filesDir, "datastore/$name.preferences_pb"). No two instances
+     * of DataStore should act on the same file at the same time.
+     */
+    public constructor(context: Context, name: String) {
+        this.context = context
+        this.name = name
+    }
+
+    /**
+     * Set the Scheduler on which to perform IO and transform operations. This is converted into
+     * a CoroutineDispatcher before being added to DataStore.
+     *
+     * This parameter is optional and defaults to Schedulers.io().
+     *
+     * @param ioScheduler the scheduler on which IO and transform operations are run
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun setIoScheduler(ioScheduler: Scheduler): RxPreferenceDataStoreBuilder =
+        apply { this.ioScheduler = ioScheduler }
+
+    /**
+     * Sets the corruption handler to install into the DataStore.
+     *
+     * This parameter is optional and defaults to no corruption handler.
+     *
+     * @param corruptionHandler the ReplaceFileCorruptionHandler to install
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun setCorruptionHandler(corruptionHandler: ReplaceFileCorruptionHandler<Preferences>):
+        RxPreferenceDataStoreBuilder = apply { this.corruptionHandler = corruptionHandler }
+
+    /**
+     * Add an RxDataMigration to the DataStore. Migrations are run in the order they are added.
+     *
+     * @param rxDataMigration the migration to add.
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun addRxDataMigration(rxDataMigration: RxDataMigration<Preferences>):
+        RxPreferenceDataStoreBuilder = apply {
+            this.dataMigrations.add(DataMigrationFromRxDataMigration(rxDataMigration))
+        }
+
+    /**
+     * Add a DataMigration to the Datastore. Migrations are run in the order they are added.
+     *
+     * @param dataMigration the migration to add
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun addDataMigration(dataMigration: DataMigration<Preferences>):
+        RxPreferenceDataStoreBuilder = apply {
+            this.dataMigrations.add(dataMigration)
+        }
+
+    /**
+     * Build the DataStore.
+     *
+     * @throws IllegalStateException if serializer is not set or if neither produceFile not
+     * context and name are set.
+     * @return the DataStore with the provided parameters
+     */
+    public fun build(): DataStore<Preferences> {
+        val scope = CoroutineScope(ioScheduler.asCoroutineDispatcher())
+
+        val produceFile: Callable<File>? = this.produceFile
+        val context: Context? = this.context
+        val name: String? = this.name
+
+        return if (produceFile != null) {
+            PreferenceDataStoreFactory.create(
+                produceFile = { produceFile.call() },
+                scope = CoroutineScope(
+                    ioScheduler.asCoroutineDispatcher()
+                ),
+                corruptionHandler = corruptionHandler,
+                migrations = dataMigrations
+            )
+        } else if (context != null && name != null) {
+            return context.createDataStore(
+                name = name,
+                scope = scope,
+                corruptionHandler = corruptionHandler,
+                migrations = dataMigrations
+            )
+        } else {
+            error("Either produceFile or context and name must be set. This should never happen.")
+        }
+    }
+}
+
+internal class DataMigrationFromRxDataMigration<T>(private val migration: RxDataMigration<T>) :
+    DataMigration<T> {
+    override suspend fun shouldMigrate(currentData: T): Boolean {
+        return migration.shouldMigrate(currentData).await()
+    }
+
+    override suspend fun migrate(currentData: T): T {
+        return migration.migrate(currentData).await()
+    }
+
+    override suspend fun cleanUp() {
+        migration.cleanUp().await()
+    }
+}
diff --git a/datastore/datastore-preferences-rxjava3/api/current.txt b/datastore/datastore-preferences-rxjava3/api/current.txt
new file mode 100644
index 0000000..88fe233
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/api/current.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.datastore.preferences.rxjava3 {
+
+  public final class RxPreferenceDataStoreBuilder {
+    ctor public RxPreferenceDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile);
+    ctor public RxPreferenceDataStoreBuilder(android.content.Context context, String name);
+    method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder addDataMigration(androidx.datastore.core.DataMigration<androidx.datastore.preferences.core.Preferences> dataMigration);
+    method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder addRxDataMigration(androidx.datastore.rxjava3.RxDataMigration<androidx.datastore.preferences.core.Preferences> rxDataMigration);
+    method public androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences> build();
+    method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.core.Preferences> corruptionHandler);
+    method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder setIoScheduler(io.reactivex.rxjava3.core.Scheduler ioScheduler);
+  }
+
+}
+
diff --git a/datastore/datastore-preferences-rxjava3/api/public_plus_experimental_current.txt b/datastore/datastore-preferences-rxjava3/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..88fe233
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/api/public_plus_experimental_current.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.datastore.preferences.rxjava3 {
+
+  public final class RxPreferenceDataStoreBuilder {
+    ctor public RxPreferenceDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile);
+    ctor public RxPreferenceDataStoreBuilder(android.content.Context context, String name);
+    method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder addDataMigration(androidx.datastore.core.DataMigration<androidx.datastore.preferences.core.Preferences> dataMigration);
+    method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder addRxDataMigration(androidx.datastore.rxjava3.RxDataMigration<androidx.datastore.preferences.core.Preferences> rxDataMigration);
+    method public androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences> build();
+    method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.core.Preferences> corruptionHandler);
+    method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder setIoScheduler(io.reactivex.rxjava3.core.Scheduler ioScheduler);
+  }
+
+}
+
diff --git a/datastore/datastore-preferences-rxjava3/api/res-current.txt b/datastore/datastore-preferences-rxjava3/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/api/res-current.txt
diff --git a/datastore/datastore-preferences-rxjava3/api/restricted_current.txt b/datastore/datastore-preferences-rxjava3/api/restricted_current.txt
new file mode 100644
index 0000000..88fe233
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/api/restricted_current.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.datastore.preferences.rxjava3 {
+
+  public final class RxPreferenceDataStoreBuilder {
+    ctor public RxPreferenceDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile);
+    ctor public RxPreferenceDataStoreBuilder(android.content.Context context, String name);
+    method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder addDataMigration(androidx.datastore.core.DataMigration<androidx.datastore.preferences.core.Preferences> dataMigration);
+    method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder addRxDataMigration(androidx.datastore.rxjava3.RxDataMigration<androidx.datastore.preferences.core.Preferences> rxDataMigration);
+    method public androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences> build();
+    method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.core.Preferences> corruptionHandler);
+    method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder setIoScheduler(io.reactivex.rxjava3.core.Scheduler ioScheduler);
+  }
+
+}
+
diff --git a/datastore/datastore-preferences-rxjava3/build.gradle b/datastore/datastore-preferences-rxjava3/build.gradle
new file mode 100644
index 0000000..44af2c9
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/build.gradle
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2020 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 static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.AndroidXExtension
+import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("kotlin-android")
+}
+
+android {
+    sourceSets {
+        test.java.srcDirs += 'src/test-common/java'
+        androidTest.java.srcDirs += 'src/test-common/java'
+    }
+}
+
+dependencies {
+    api(KOTLIN_STDLIB)
+    api(KOTLIN_COROUTINES_CORE)
+    api("androidx.annotation:annotation:1.1.0")
+    api(RX_JAVA3)
+
+    api(project(":datastore:datastore"))
+    api(project(":datastore:datastore-rxjava3"))
+    api(project(":datastore:datastore-preferences"))
+
+    implementation(KOTLIN_COROUTINES_RX3)
+
+    testImplementation(JUNIT)
+    testImplementation(KOTLIN_COROUTINES_TEST)
+    testImplementation(TRUTH)
+    testImplementation(project(":internal-testutils-truth"))
+
+    androidTestImplementation(project(":datastore:datastore-core"))
+    androidTestImplementation(project(":datastore:datastore"))
+    androidTestImplementation(JUNIT)
+    androidTestImplementation(project(":internal-testutils-truth"))
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    androidTestImplementation(ANDROIDX_TEST_CORE)
+}
+
+androidx {
+    name = "Android DataStore Core RxJava2 Wrappers"
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    mavenGroup = LibraryGroups.DATASTORE
+    inceptionYear = "2020"
+    description = "Android DataStore Core - contains wrappers for using DataStore using RxJava2"
+    legacyDisableKotlinStrictApiMode = true
+}
diff --git a/datastore/datastore-preferences-rxjava3/src/androidTest/java/androidx/datastore/preferences/rxjava3/RxPreferencesDataStoreBuilderTest.java b/datastore/datastore-preferences-rxjava3/src/androidTest/java/androidx/datastore/preferences/rxjava3/RxPreferencesDataStoreBuilderTest.java
new file mode 100644
index 0000000..f72a779
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/src/androidTest/java/androidx/datastore/preferences/rxjava3/RxPreferencesDataStoreBuilderTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2020 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.datastore.preferences.rxjava3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.datastore.core.DataStore;
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler;
+import androidx.datastore.preferences.core.MutablePreferences;
+import androidx.datastore.preferences.core.Preferences;
+import androidx.datastore.preferences.core.PreferencesFactory;
+import androidx.datastore.preferences.core.PreferencesKeys;
+import androidx.datastore.rxjava3.RxDataMigration;
+import androidx.datastore.rxjava3.RxDataStore;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+import io.reactivex.rxjava3.core.Completable;
+import io.reactivex.rxjava3.core.Single;
+
+
+public class RxPreferencesDataStoreBuilderTest {
+    @Rule
+    public TemporaryFolder tempFolder = new TemporaryFolder();
+
+    private static final Preferences.Key<Integer> INTEGER_KEY =
+            PreferencesKeys.intKey("int_key");
+
+    private static Single<Preferences> incrementInteger(Preferences preferencesIn) {
+        MutablePreferences prefs = preferencesIn.toMutablePreferences();
+        Integer currentInt = prefs.get(INTEGER_KEY);
+        prefs.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1);
+        return Single.just(prefs);
+    }
+
+    @Test
+    public void testConstructWithProduceFile() throws Exception {
+        File file = tempFolder.newFile("temp.preferences_pb");
+
+        DataStore<Preferences> dataStore =
+                new RxPreferenceDataStoreBuilder(() -> file).build();
+
+        Single<Preferences> incrementInt = RxDataStore.updateDataAsync(dataStore,
+                RxPreferencesDataStoreBuilderTest::incrementInteger);
+        assertThat(incrementInt.blockingGet().get(INTEGER_KEY)).isEqualTo(1);
+
+        // Construct it again and confirm that the data is still there:
+        dataStore = new RxPreferenceDataStoreBuilder(() -> file).build();
+
+        assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY))
+                .isEqualTo(1);
+    }
+
+
+    @Test
+    public void testConstructWithContextAndName() throws Exception {
+
+        Context context = ApplicationProvider.getApplicationContext();
+        String name = "my_data_store";
+
+        File prefsFile = new File(context.getFilesDir().getPath()
+                + "/datastore/" + name + ".preferences_pb");
+        if (prefsFile.exists()) {
+            prefsFile.delete();
+        }
+
+        DataStore<Preferences> dataStore =
+                new RxPreferenceDataStoreBuilder(context, name).build();
+
+        Single<Preferences> set1 = RxDataStore.updateDataAsync(dataStore,
+                RxPreferencesDataStoreBuilderTest::incrementInteger);
+        assertThat(set1.blockingGet().get(INTEGER_KEY)).isEqualTo(1);
+
+        // Construct it again and confirm that the data is still there:
+        dataStore = new RxPreferenceDataStoreBuilder(context, name).build();
+        assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY)).isEqualTo(1);
+
+        // Construct it again with the expected file path and confirm that the data is there:
+        dataStore =
+                new RxPreferenceDataStoreBuilder(
+                        () ->
+                                new File(context.getFilesDir().getPath()
+                                        + "/datastore/" + name + ".preferences_pb")
+                ).build();
+
+        assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY)).isEqualTo(1);
+    }
+
+    @Test
+    public void testMigrationsAreInstalledAndRun() throws Exception {
+        RxDataMigration<Preferences> plusOneMigration = new RxDataMigration<Preferences>() {
+            @NonNull
+            @Override
+            public Single<Boolean> shouldMigrate(@NonNull Preferences currentData) {
+                return Single.just(true);
+            }
+
+            @NonNull
+            @Override
+            public Single<Preferences> migrate(@NonNull Preferences currentData) {
+                return incrementInteger(currentData);
+            }
+
+            @NonNull
+            @Override
+            public Completable cleanUp() {
+                return Completable.complete();
+            }
+        };
+
+        DataStore<Preferences> dataStore =
+                new RxPreferenceDataStoreBuilder(() ->
+                        tempFolder.newFile("temp.preferences_pb"))
+                        .addRxDataMigration(plusOneMigration)
+                        .build();
+
+        assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY))
+                .isEqualTo(1);
+    }
+
+
+    @Test
+    public void testCorruptionHandlerIsUser() throws Exception {
+
+        File file = tempFolder.newFile("temp.preferences_pb");
+
+        try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
+            fileOutputStream.write(0); // will cause corruption exception
+        }
+
+        ReplaceFileCorruptionHandler<Preferences> replaceFileCorruptionHandler =
+                new ReplaceFileCorruptionHandler<Preferences>(exception -> {
+                    MutablePreferences mutablePreferences =
+                            PreferencesFactory.createMutable();
+                    mutablePreferences.set(INTEGER_KEY, 99);
+                    return (Preferences) mutablePreferences;
+                });
+
+
+        DataStore<Preferences> dataStore =
+                new RxPreferenceDataStoreBuilder(() -> file)
+                        .setCorruptionHandler(replaceFileCorruptionHandler)
+                        .build();
+
+        assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY))
+                .isEqualTo(99);
+    }
+}
diff --git a/datastore/datastore-preferences-rxjava3/src/main/AndroidManifest.xml b/datastore/datastore-preferences-rxjava3/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..13f23ea
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2020 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.datastore.preferences.rxjava3">
+
+</manifest>
diff --git a/datastore/datastore-preferences-rxjava3/src/main/java/androidx/datastore/preferences/rxjava3/RxPreferenceDataStoreBuilder.kt b/datastore/datastore-preferences-rxjava3/src/main/java/androidx/datastore/preferences/rxjava3/RxPreferenceDataStoreBuilder.kt
new file mode 100644
index 0000000..527cfb3
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/src/main/java/androidx/datastore/preferences/rxjava3/RxPreferenceDataStoreBuilder.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2020 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.datastore.preferences.rxjava3
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.datastore.core.DataMigration
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.createDataStore
+import androidx.datastore.rxjava3.RxDataMigration
+import io.reactivex.rxjava3.core.Scheduler
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.rx3.asCoroutineDispatcher
+import kotlinx.coroutines.rx3.await
+import java.io.File
+import java.util.concurrent.Callable
+
+/**
+ * RxSharedPreferencesMigrationBuilder class for a DataStore that works on a single process.
+ */
+@SuppressLint("TopLevelBuilder")
+public class RxPreferenceDataStoreBuilder {
+
+    // Either produceFile or context & name must be set, but not both.
+    private var produceFile: Callable<File>? = null
+
+    private var context: Context? = null
+    private var name: String? = null
+
+    // Optional
+    private var ioScheduler: Scheduler = Schedulers.io()
+    private var corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null
+    private val dataMigrations: MutableList<DataMigration<Preferences>> = mutableListOf()
+
+    /**
+     * Create a RxPreferenceDataStoreBuilder with the callable which returns the File that
+     * DataStore acts on. The user is responsible for ensuring that there is never more than one
+     * DataStore acting on a file at a time.
+     *
+     * @param produceFile Function which returns the file that the new DataStore will act on. The
+     * function must return the same path every time. No two instances of DataStore should act on
+     * the same file at the same time.
+     */
+    public constructor(produceFile: Callable<File>) {
+        this.produceFile = produceFile
+    }
+
+    /**
+     * Create a RxPreferenceDataStoreBuilder with the Context and name from which to derive the
+     * DataStore file. The file is generated by See [Context.createDataStore] for more info. The
+     * user is responsible for ensuring that there is never more than one DataStore acting on a
+     * file at a time.
+     *
+     * @param context the context from which we retrieve files directory.
+     * @param name the filename relative to Context.filesDir that DataStore acts on. The File is
+     * obtained by calling File(this.filesDir, "datastore/$name.preferences_pb"). No two instances
+     * of DataStore should act on the same file at the same time.
+     */
+    public constructor(context: Context, name: String) {
+        this.context = context
+        this.name = name
+    }
+
+    /**
+     * Set the Scheduler on which to perform IO and transform operations. This is converted into
+     * a CoroutineDispatcher before being added to DataStore.
+     *
+     * This parameter is optional and defaults to Schedulers.io().
+     *
+     * @param ioScheduler the scheduler on which IO and transform operations are run
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun setIoScheduler(ioScheduler: Scheduler): RxPreferenceDataStoreBuilder =
+        apply { this.ioScheduler = ioScheduler }
+
+    /**
+     * Sets the corruption handler to install into the DataStore.
+     *
+     * This parameter is optional and defaults to no corruption handler.
+     *
+     * @param corruptionHandler
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun setCorruptionHandler(corruptionHandler: ReplaceFileCorruptionHandler<Preferences>):
+        RxPreferenceDataStoreBuilder = apply { this.corruptionHandler = corruptionHandler }
+
+    /**
+     * Add an RxDataMigration to the DataStore. Migrations are run in the order they are added.
+     *
+     * @param rxDataMigration the migration to add.
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun addRxDataMigration(rxDataMigration: RxDataMigration<Preferences>):
+        RxPreferenceDataStoreBuilder = apply {
+            this.dataMigrations.add(DataMigrationFromRxDataMigration(rxDataMigration))
+        }
+
+    /**
+     * Add a DataMigration to the Datastore. Migrations are run in the order they are added.
+     *
+     * @param dataMigration the migration to add
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun addDataMigration(dataMigration: DataMigration<Preferences>):
+        RxPreferenceDataStoreBuilder = apply {
+            this.dataMigrations.add(dataMigration)
+        }
+
+    /**
+     * Build the DataStore.
+     *
+     * @throws IllegalStateException if serializer is not set or if neither produceFile not
+     * context and name are set.
+     * @return the DataStore with the provided parameters
+     */
+    public fun build(): DataStore<Preferences> {
+        val scope = CoroutineScope(ioScheduler.asCoroutineDispatcher())
+
+        val produceFile: Callable<File>? = this.produceFile
+        val context: Context? = this.context
+        val name: String? = this.name
+
+        return if (produceFile != null) {
+            PreferenceDataStoreFactory.create(
+                produceFile = { produceFile.call() },
+                scope = CoroutineScope(
+                    ioScheduler.asCoroutineDispatcher()
+                ),
+                corruptionHandler = corruptionHandler,
+                migrations = dataMigrations
+            )
+        } else if (context != null && name != null) {
+            return context.createDataStore(
+                name = name,
+                scope = scope,
+                corruptionHandler = corruptionHandler,
+                migrations = dataMigrations
+            )
+        } else {
+            error("Either produceFile or context and name must be set. This should never happen.")
+        }
+    }
+}
+
+internal class DataMigrationFromRxDataMigration<T>(private val migration: RxDataMigration<T>) :
+    DataMigration<T> {
+    override suspend fun shouldMigrate(currentData: T): Boolean {
+        return migration.shouldMigrate(currentData).await()
+    }
+
+    override suspend fun migrate(currentData: T): T {
+        return migration.migrate(currentData).await()
+    }
+
+    override suspend fun cleanUp() {
+        migration.cleanUp().await()
+    }
+}
diff --git a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.kt b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.kt
deleted file mode 100644
index cc1f3493..0000000
--- a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2020 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.datastore.rxjava2
-
-@Suppress("UNCHECKED_CAST")
-internal fun <T : Throwable?> assertThrows(
-    expectedType: Class<T>,
-    runnable: Runnable
-): T {
-    try {
-        runnable.run()
-    } catch (t: Throwable) {
-        if (!expectedType.isInstance(t)) {
-            throw RuntimeException(t)
-        }
-        return t as T
-    }
-    throw AssertionError(
-        String.format(
-            "Expected %s wasn't thrown",
-            expectedType.simpleName
-        )
-    )
-}
diff --git a/datastore/datastore-rxjava3/api/current.txt b/datastore/datastore-rxjava3/api/current.txt
new file mode 100644
index 0000000..bdba98a
--- /dev/null
+++ b/datastore/datastore-rxjava3/api/current.txt
@@ -0,0 +1,38 @@
+// Signature format: 4.0
+package androidx.datastore.rxjava3 {
+
+  public interface RxDataMigration<T> {
+    method public io.reactivex.rxjava3.core.Completable cleanUp();
+    method public io.reactivex.rxjava3.core.Single<T!> migrate(T?);
+    method public io.reactivex.rxjava3.core.Single<java.lang.Boolean!> shouldMigrate(T?);
+  }
+
+  public final class RxDataStore {
+    method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Flowable<T> data(androidx.datastore.core.DataStore<T>);
+    method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.rxjava3.functions.Function<T,io.reactivex.rxjava3.core.Single<T>> transform);
+  }
+
+  public final class RxDataStoreBuilder<T> {
+    ctor public RxDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile, androidx.datastore.core.Serializer<T> serializer);
+    ctor public RxDataStoreBuilder(android.content.Context context, String fileName, androidx.datastore.core.Serializer<T> serializer);
+    method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> addDataMigration(androidx.datastore.core.DataMigration<T> dataMigration);
+    method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> addRxDataMigration(androidx.datastore.rxjava3.RxDataMigration<T> rxDataMigration);
+    method public androidx.datastore.core.DataStore<T> build();
+    method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T> corruptionHandler);
+    method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> setIoScheduler(io.reactivex.rxjava3.core.Scheduler ioScheduler);
+  }
+
+  public interface RxSharedPreferencesMigration<T> {
+    method public io.reactivex.rxjava3.core.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
+    method public default io.reactivex.rxjava3.core.Single<java.lang.Boolean> shouldMigrate(T? currentData);
+  }
+
+  public final class RxSharedPreferencesMigrationBuilder<T> {
+    ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava3.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
+    method public androidx.datastore.core.DataMigration<T> build();
+    method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
+    method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
+  }
+
+}
+
diff --git a/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt b/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..bdba98a
--- /dev/null
+++ b/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt
@@ -0,0 +1,38 @@
+// Signature format: 4.0
+package androidx.datastore.rxjava3 {
+
+  public interface RxDataMigration<T> {
+    method public io.reactivex.rxjava3.core.Completable cleanUp();
+    method public io.reactivex.rxjava3.core.Single<T!> migrate(T?);
+    method public io.reactivex.rxjava3.core.Single<java.lang.Boolean!> shouldMigrate(T?);
+  }
+
+  public final class RxDataStore {
+    method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Flowable<T> data(androidx.datastore.core.DataStore<T>);
+    method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.rxjava3.functions.Function<T,io.reactivex.rxjava3.core.Single<T>> transform);
+  }
+
+  public final class RxDataStoreBuilder<T> {
+    ctor public RxDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile, androidx.datastore.core.Serializer<T> serializer);
+    ctor public RxDataStoreBuilder(android.content.Context context, String fileName, androidx.datastore.core.Serializer<T> serializer);
+    method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> addDataMigration(androidx.datastore.core.DataMigration<T> dataMigration);
+    method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> addRxDataMigration(androidx.datastore.rxjava3.RxDataMigration<T> rxDataMigration);
+    method public androidx.datastore.core.DataStore<T> build();
+    method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T> corruptionHandler);
+    method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> setIoScheduler(io.reactivex.rxjava3.core.Scheduler ioScheduler);
+  }
+
+  public interface RxSharedPreferencesMigration<T> {
+    method public io.reactivex.rxjava3.core.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
+    method public default io.reactivex.rxjava3.core.Single<java.lang.Boolean> shouldMigrate(T? currentData);
+  }
+
+  public final class RxSharedPreferencesMigrationBuilder<T> {
+    ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava3.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
+    method public androidx.datastore.core.DataMigration<T> build();
+    method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
+    method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
+  }
+
+}
+
diff --git a/datastore/datastore-rxjava3/api/res-current.txt b/datastore/datastore-rxjava3/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/datastore/datastore-rxjava3/api/res-current.txt
diff --git a/datastore/datastore-rxjava3/api/restricted_current.txt b/datastore/datastore-rxjava3/api/restricted_current.txt
new file mode 100644
index 0000000..bdba98a
--- /dev/null
+++ b/datastore/datastore-rxjava3/api/restricted_current.txt
@@ -0,0 +1,38 @@
+// Signature format: 4.0
+package androidx.datastore.rxjava3 {
+
+  public interface RxDataMigration<T> {
+    method public io.reactivex.rxjava3.core.Completable cleanUp();
+    method public io.reactivex.rxjava3.core.Single<T!> migrate(T?);
+    method public io.reactivex.rxjava3.core.Single<java.lang.Boolean!> shouldMigrate(T?);
+  }
+
+  public final class RxDataStore {
+    method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Flowable<T> data(androidx.datastore.core.DataStore<T>);
+    method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.rxjava3.functions.Function<T,io.reactivex.rxjava3.core.Single<T>> transform);
+  }
+
+  public final class RxDataStoreBuilder<T> {
+    ctor public RxDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile, androidx.datastore.core.Serializer<T> serializer);
+    ctor public RxDataStoreBuilder(android.content.Context context, String fileName, androidx.datastore.core.Serializer<T> serializer);
+    method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> addDataMigration(androidx.datastore.core.DataMigration<T> dataMigration);
+    method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> addRxDataMigration(androidx.datastore.rxjava3.RxDataMigration<T> rxDataMigration);
+    method public androidx.datastore.core.DataStore<T> build();
+    method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T> corruptionHandler);
+    method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> setIoScheduler(io.reactivex.rxjava3.core.Scheduler ioScheduler);
+  }
+
+  public interface RxSharedPreferencesMigration<T> {
+    method public io.reactivex.rxjava3.core.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
+    method public default io.reactivex.rxjava3.core.Single<java.lang.Boolean> shouldMigrate(T? currentData);
+  }
+
+  public final class RxSharedPreferencesMigrationBuilder<T> {
+    ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava3.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
+    method public androidx.datastore.core.DataMigration<T> build();
+    method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
+    method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
+  }
+
+}
+
diff --git a/datastore/datastore-rxjava3/build.gradle b/datastore/datastore-rxjava3/build.gradle
new file mode 100644
index 0000000..9f8e453
--- /dev/null
+++ b/datastore/datastore-rxjava3/build.gradle
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 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 static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.AndroidXExtension
+import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("kotlin-android")
+}
+
+android {
+    sourceSets {
+        test.java.srcDirs += 'src/test-common/java'
+        androidTest.java.srcDirs += 'src/test-common/java'
+    }
+}
+
+dependencies {
+    api(KOTLIN_STDLIB)
+    api(KOTLIN_COROUTINES_CORE)
+    api("androidx.annotation:annotation:1.1.0")
+    api(RX_JAVA3)
+
+    api(project(":datastore:datastore"))
+
+    implementation(KOTLIN_COROUTINES_RX3)
+
+    testImplementation(JUNIT)
+    testImplementation(KOTLIN_COROUTINES_TEST)
+    testImplementation(TRUTH)
+    testImplementation(project(":internal-testutils-truth"))
+
+    androidTestImplementation(JUNIT)
+    androidTestImplementation(project(":internal-testutils-truth"))
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    androidTestImplementation(ANDROIDX_TEST_CORE)
+}
+
+androidx {
+    name = "Android DataStore Core RxJava2 Wrappers"
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    mavenGroup = LibraryGroups.DATASTORE
+    inceptionYear = "2020"
+    description = "Android DataStore Core - contains wrappers for using DataStore using RxJava2"
+    legacyDisableKotlinStrictApiMode = true
+}
diff --git a/datastore/datastore-rxjava3/src/androidTest/AndroidManifest.xml b/datastore/datastore-rxjava3/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..3369992
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2020 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.datastore.rxjava3">
+
+</manifest>
diff --git a/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxDataStoreBuilderTest.java b/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxDataStoreBuilderTest.java
new file mode 100644
index 0000000..0564f77
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxDataStoreBuilderTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2020 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.datastore.rxjava3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.datastore.core.DataStore;
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import io.reactivex.rxjava3.core.Completable;
+import io.reactivex.rxjava3.core.Scheduler;
+import io.reactivex.rxjava3.core.Single;
+import io.reactivex.rxjava3.schedulers.Schedulers;
+
+public class RxDataStoreBuilderTest {
+    @Rule
+    public TemporaryFolder tempFolder = new TemporaryFolder();
+
+    private static Single<Byte> incrementByte(Byte byteIn) {
+        return Single.just(++byteIn);
+    }
+
+    @Test
+    public void testConstructWithProduceFile() throws Exception {
+        File file = tempFolder.newFile();
+        DataStore<Byte> dataStore =
+                new RxDataStoreBuilder<Byte>(() -> file, new TestingSerializer())
+                        .build();
+        Single<Byte> incrementByte = RxDataStore.updateDataAsync(dataStore,
+                RxDataStoreBuilderTest::incrementByte);
+        assertThat(incrementByte.blockingGet()).isEqualTo(1);
+        // Construct it again and confirm that the data is still there:
+        dataStore =
+                new RxDataStoreBuilder<Byte>(() -> file, new TestingSerializer())
+                        .build();
+        assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
+    }
+
+    @Test
+    public void testConstructWithContextAndName() throws Exception {
+        Context context = ApplicationProvider.getApplicationContext();
+        String name = "my_data_store";
+        DataStore<Byte> dataStore =
+                new RxDataStoreBuilder<Byte>(context, name, new TestingSerializer())
+                        .build();
+        Single<Byte> set1 = RxDataStore.updateDataAsync(dataStore, input -> Single.just((byte) 1));
+        assertThat(set1.blockingGet()).isEqualTo(1);
+        // Construct it again and confirm that the data is still there:
+        dataStore =
+                new RxDataStoreBuilder<Byte>(context, name, new TestingSerializer())
+                        .build();
+        assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
+        // Construct it again with the expected file path and confirm that the data is there:
+        dataStore =
+                new RxDataStoreBuilder<Byte>(() -> new File(context.getFilesDir().getPath()
+                        + "/datastore/" + name), new TestingSerializer()
+                )
+                        .build();
+        assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
+    }
+
+    @Test
+    public void testMigrationsAreInstalledAndRun() throws Exception {
+        RxDataMigration<Byte> plusOneMigration = new RxDataMigration<Byte>() {
+            @NonNull
+            @Override
+            public Single<Boolean> shouldMigrate(@NonNull Byte currentData) {
+                return Single.just(true);
+            }
+
+            @NonNull
+            @Override
+            public Single<Byte> migrate(@NonNull Byte currentData) {
+                return incrementByte(currentData);
+            }
+
+            @NonNull
+            @Override
+            public Completable cleanUp() {
+                return Completable.complete();
+            }
+        };
+
+        DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(
+                () -> tempFolder.newFile(), new TestingSerializer())
+                .addRxDataMigration(plusOneMigration)
+                .build();
+
+        assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
+    }
+
+    @Test
+    public void testSpecifiedSchedulerIsUser() throws Exception {
+        Scheduler singleThreadedScheduler =
+                Schedulers.from(Executors.newSingleThreadExecutor(new ThreadFactory() {
+                    @Override
+                    public Thread newThread(Runnable r) {
+                        return new Thread(r, "TestingThread");
+                    }
+                }));
+
+
+        DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(() -> tempFolder.newFile(),
+                new TestingSerializer())
+                .setIoScheduler(singleThreadedScheduler)
+                .build();
+        Single<Byte> update = RxDataStore.updateDataAsync(dataStore, input -> {
+            Thread currentThread = Thread.currentThread();
+            assertThat(currentThread.getName()).isEqualTo("TestingThread");
+            return Single.just(input);
+        });
+        assertThat(update.blockingGet()).isEqualTo((byte) 0);
+        Single<Byte> subsequentUpdate = RxDataStore.updateDataAsync(dataStore, input -> {
+            Thread currentThread = Thread.currentThread();
+            assertThat(currentThread.getName()).isEqualTo("TestingThread");
+            return Single.just(input);
+        });
+        assertThat(subsequentUpdate.blockingGet()).isEqualTo((byte) 0);
+    }
+
+    @Test
+    public void testCorruptionHandlerIsUser() {
+        TestingSerializer testingSerializer = new TestingSerializer();
+        testingSerializer.setFailReadWithCorruptionException(true);
+        ReplaceFileCorruptionHandler<Byte> replaceFileCorruptionHandler =
+                new ReplaceFileCorruptionHandler<Byte>(exception -> (byte) 99);
+
+
+        DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(
+                () -> tempFolder.newFile(),
+                testingSerializer)
+                .setCorruptionHandler(replaceFileCorruptionHandler)
+                .build();
+        assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(99);
+    }
+}
diff --git a/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxSharedPreferencesMigrationTest.java b/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxSharedPreferencesMigrationTest.java
new file mode 100644
index 0000000..b66d6ef
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxSharedPreferencesMigrationTest.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2020 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.datastore.rxjava3;
+
+import static androidx.testutils.AssertionsKt.assertThrows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.datastore.core.DataMigration;
+import androidx.datastore.core.DataStore;
+import androidx.datastore.migrations.SharedPreferencesView;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.truth.Truth;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+
+import io.reactivex.rxjava3.core.Single;
+
+public class RxSharedPreferencesMigrationTest {
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    private final String mSharedPrefsName = "shared_prefs_name";
+
+
+    private Context mContext;
+    private SharedPreferences mSharedPrefs;
+    private File mDatastoreFile;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = ApplicationProvider.getApplicationContext();
+        mSharedPrefs = mContext.getSharedPreferences(mSharedPrefsName, Context.MODE_PRIVATE);
+        mDatastoreFile = temporaryFolder.newFile("test_file.preferences_pb");
+
+        assertThat(mSharedPrefs.edit().clear().commit()).isTrue();
+    }
+
+    @Test
+    public void testShouldMigrateSkipsMigration() {
+        RxSharedPreferencesMigration<Byte> skippedMigration =
+                new RxSharedPreferencesMigration<Byte>() {
+                    @NotNull
+                    @Override
+                    public Single<Boolean> shouldMigrate(Byte currentData) {
+                        return Single.just(false);
+                    }
+
+                    @NotNull
+                    @Override
+                    public Single<Byte> migrate(
+                            @NotNull SharedPreferencesView sharedPreferencesView,
+                            Byte currentData) {
+                        return Single.error(
+                                new IllegalStateException("We shouldn't reach this point!"));
+                    }
+                };
+
+
+        DataMigration<Byte> spMigration =
+                getSpMigrationBuilder(skippedMigration).build();
+
+        DataStore<Byte> dataStoreWithMigrations = getDataStoreWithMigration(spMigration);
+
+        Truth.assertThat(RxDataStore.data(dataStoreWithMigrations).blockingFirst()).isEqualTo(0);
+    }
+
+    @Test
+    public void testSharedPrefsViewContainsSpecifiedKeys() {
+        String includedKey = "key1";
+        int includedVal = 99;
+        String notMigratedKey = "key2";
+
+        assertThat(mSharedPrefs.edit().putInt(includedKey, includedVal).putInt(notMigratedKey,
+                123).commit()).isTrue();
+
+        DataMigration<Byte> dataMigration =
+                getSpMigrationBuilder(
+                        new DefaultMigration() {
+                            @NotNull
+                            @Override
+                            public Single<Byte> migrate(
+                                    @NotNull SharedPreferencesView sharedPreferencesView,
+                                    Byte currentData) {
+                                assertThat(sharedPreferencesView.contains(includedKey)).isTrue();
+                                assertThat(sharedPreferencesView.getAll().size()).isEqualTo(1);
+                                assertThrows(IllegalStateException.class,
+                                        () -> sharedPreferencesView.getInt(notMigratedKey, -1));
+
+                                return Single.just((byte) 50);
+                            }
+                        }
+                ).setKeysToMigrate(includedKey).build();
+
+        DataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
+
+        assertThat(RxDataStore.data(byteStore).blockingFirst()).isEqualTo(50);
+
+        assertThat(mSharedPrefs.contains(includedKey)).isFalse();
+        assertThat(mSharedPrefs.contains(notMigratedKey)).isTrue();
+    }
+
+
+    @Test
+    public void testSharedPrefsViewWithAllKeysSpecified() {
+        String includedKey = "key1";
+        String includedKey2 = "key2";
+        int value = 99;
+
+        assertThat(mSharedPrefs.edit().putInt(includedKey, value).putInt(includedKey2,
+                value).commit()).isTrue();
+
+        DataMigration<Byte> dataMigration =
+                getSpMigrationBuilder(
+                        new DefaultMigration() {
+                            @NotNull
+                            @Override
+                            public Single<Byte> migrate(
+                                    @NotNull SharedPreferencesView sharedPreferencesView,
+                                    Byte currentData) {
+                                assertThat(sharedPreferencesView.contains(includedKey)).isTrue();
+                                assertThat(sharedPreferencesView.contains(includedKey2)).isTrue();
+                                assertThat(sharedPreferencesView.getAll().size()).isEqualTo(2);
+
+                                return Single.just((byte) 50);
+                            }
+                        }
+                ).build();
+
+        DataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
+
+        assertThat(RxDataStore.data(byteStore).blockingFirst()).isEqualTo(50);
+
+        assertThat(mSharedPrefs.contains(includedKey)).isFalse();
+        assertThat(mSharedPrefs.contains(includedKey2)).isFalse();
+    }
+
+    @Test
+    public void testDeletesEmptySharedPreferences() {
+        String key = "key";
+        String value = "value";
+        assertThat(mSharedPrefs.edit().putString(key, value).commit()).isTrue();
+
+        DataMigration<Byte> dataMigration =
+                getSpMigrationBuilder(new DefaultMigration()).setDeleteEmptyPreferences(
+                        true).build();
+        DataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
+        assertThat(RxDataStore.data(byteStore).blockingFirst()).isEqualTo(0);
+
+        // Check that the shared preferences files are deleted
+        File prefsDir = new File(mContext.getApplicationInfo().dataDir, "shared_prefs");
+        File prefsFile = new File(prefsDir, mSharedPrefsName + ".xml");
+        File backupPrefsFile = new File(prefsFile.getPath() + ".bak");
+        assertThat(prefsFile.exists()).isFalse();
+        assertThat(backupPrefsFile.exists()).isFalse();
+    }
+
+    private RxSharedPreferencesMigrationBuilder<Byte> getSpMigrationBuilder(
+            RxSharedPreferencesMigration<Byte> rxSharedPreferencesMigration) {
+        return new RxSharedPreferencesMigrationBuilder<Byte>(mContext, mSharedPrefsName,
+                rxSharedPreferencesMigration);
+    }
+
+    private DataStore<Byte> getDataStoreWithMigration(DataMigration<Byte> dataMigration) {
+        return new RxDataStoreBuilder<Byte>(() -> mDatastoreFile, new TestingSerializer())
+                .addDataMigration(dataMigration).build();
+    }
+
+
+    private static class DefaultMigration implements RxSharedPreferencesMigration<Byte> {
+
+        @NotNull
+        @Override
+        public Single<Boolean> shouldMigrate(Byte currentData) {
+            return Single.just(true);
+        }
+
+        @NotNull
+        @Override
+        public Single<Byte> migrate(@NotNull SharedPreferencesView sharedPreferencesView,
+                Byte currentData) {
+            return Single.just(currentData);
+        }
+    }
+}
diff --git a/datastore/datastore-rxjava3/src/main/AndroidManifest.xml b/datastore/datastore-rxjava3/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3369992
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2020 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.datastore.rxjava3">
+
+</manifest>
diff --git a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataMigration.java b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataMigration.java
new file mode 100644
index 0000000..ade2acc
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataMigration.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 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.datastore.rxjava3;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import io.reactivex.rxjava3.core.Completable;
+import io.reactivex.rxjava3.core.Single;
+
+/**
+ * Interface for migrations to DataStore. Methods on this migration ([shouldMigrate], [migrate]
+ * and [cleanUp]) may be called multiple times, so their implementations must be idempotent.
+ * These methods may be called multiple times if DataStore encounters issues when writing the
+ * newly migrated data to disk or if any migration installed in the same DataStore throws an
+ * Exception.
+ *
+ * If you're migrating from SharedPreferences see [SharedPreferencesMigration].
+ *
+ * @param <T> the exception type
+ */
+public interface RxDataMigration<T> {
+
+    /**
+     * Return whether this migration needs to be performed. If this returns false, no migration or
+     * cleanup will occur. Apps should do the cheapest possible check to determine if this migration
+     * should run, since this will be called every time the DataStore is initialized. This method
+     * may be run multiple times when any failure is encountered.
+     *
+     * Note that this will always be called before each call to [migrate].
+     *
+     * @param currentData the current data (which might already populated from previous runs of this
+     *                    or other migrations). Only Nullable if the type used with DataStore is
+     *                    Nullable.
+     */
+    @NonNull
+    Single<Boolean> shouldMigrate(@Nullable T currentData);
+
+    /**
+     * Perform the migration. Implementations should be idempotent since this may be called
+     * multiple times. If migrate fails, DataStore will not commit any data to disk, cleanUp will
+     * not be called, and the exception will be propagated back to the DataStore call that
+     * triggered the migration. Future calls to DataStore will result in DataMigrations being
+     * attempted again. This method may be run multiple times when any failure is encountered.
+     *
+     * Note that this will always be called before a call to [cleanUp].
+     *
+     * @param currentData the current data (it might be populated from other migrations or from
+     *                    manual changes before this migration was added to the app). Only
+     *                    Nullable if the type used with DataStore is Nullable.
+     * @return The migrated data.
+     */
+    @NonNull
+    Single<T> migrate(@Nullable T currentData);
+
+    /**
+     * Clean up any old state/data that was migrated into the DataStore. This will not be called
+     * if the migration fails. If cleanUp throws an exception, the exception will be propagated
+     * back to the DataStore call that triggered the migration and future calls to DataStore will
+     * result in DataMigrations being attempted again. This method may be run multiple times when
+     * any failure is encountered.
+     */
+    @NonNull
+    Completable cleanUp();
+}
diff --git a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataStore.kt b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataStore.kt
new file mode 100644
index 0000000..467e039
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataStore.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2020 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.
+ */
+@file:JvmName("RxDataStore")
+
+package androidx.datastore.rxjava3
+
+import androidx.datastore.core.DataStore
+import io.reactivex.rxjava3.core.Flowable
+import io.reactivex.rxjava3.core.Single
+import io.reactivex.rxjava3.functions.Function
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.rx3.asFlowable
+import kotlinx.coroutines.rx3.asSingle
+import kotlinx.coroutines.rx3.await
+
+/**
+ * Gets a reactivex.Flowable of the data from DataStore. See [DataStore.data] for more information.
+ *
+ * Provides efficient, cached (when possible) access to the latest durably persisted state.
+ * The flow will always either emit a value or throw an exception encountered when attempting
+ * to read from disk. If an exception is encountered, collecting again will attempt to read the
+ * data again.
+ *
+ * Do not layer a cache on top of this API: it will be be impossible to guarantee consistency.
+ * Instead, use data.first() to access a single snapshot.
+ *
+ * The Flowable will complete with an IOException when an exception is encountered when reading
+ * data.
+ *
+ * @return a flow representing the current state of the data
+ */
+@ExperimentalCoroutinesApi
+public fun <T : Any> DataStore<T>.data(): Flowable<T> {
+    return this.data.asFlowable()
+}
+
+/**
+ * See [DataStore.updateData]
+ *
+ * Updates the data transactionally in an atomic read-modify-write operation. All operations
+ * are serialized, and the transform itself is a async so it can perform heavy work
+ * such as RPCs.
+ *
+ * The Single completes when the data has been persisted durably to disk (after which
+ * [data] will reflect the update). If the transform or write to disk fails, the
+ * transaction is aborted and the returned Single is completed with the error.
+ *
+ * The transform will be run on the scheduler that DataStore was constructed with.
+ *
+ * @return the snapshot returned by the transform
+ * @throws Exception when thrown by the transform function
+ */
+@ExperimentalCoroutinesApi
+public fun <T : Any> DataStore<T>.updateDataAsync(transform: Function<T, Single<T>>): Single<T> {
+    return CoroutineScope(Dispatchers.Unconfined).async {
+        this@updateDataAsync.updateData {
+            transform.apply(it).await()
+        }
+    }.asSingle(Dispatchers.Unconfined)
+}
diff --git a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataStoreBuilder.kt b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataStoreBuilder.kt
new file mode 100644
index 0000000..139bbca
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataStoreBuilder.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2020 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.datastore.rxjava3
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.datastore.core.DataMigration
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.core.Serializer
+import androidx.datastore.createDataStore
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
+import io.reactivex.rxjava3.core.Scheduler
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.rx3.asCoroutineDispatcher
+import kotlinx.coroutines.rx3.await
+import java.io.File
+import java.util.concurrent.Callable
+
+/**
+ * RxSharedPreferencesMigrationBuilder class for a DataStore that works on a single process.
+ */
+@SuppressLint("TopLevelBuilder")
+public class RxDataStoreBuilder<T> {
+
+    /**
+     * Create a RxDataStoreBuilder with the callable which returns the File that DataStore acts on.
+     * The user is responsible for ensuring that there is never more than one DataStore acting on
+     * a file at a time.
+     *
+     * @param produceFile Function which returns the file that the new DataStore will act on. The
+     * function must return the same path every time. No two instances of DataStore should act on
+     * the same file at the same time.
+     * @param serializer the serializer for the type that this DataStore acts on.
+     */
+    public constructor(produceFile: Callable<File>, serializer: Serializer<T>) {
+        this.produceFile = produceFile
+        this.serializer = serializer
+    }
+
+    /**
+     * Create a RxDataStoreBuilder with the Context and name from which to derive the DataStore
+     * file. The file is generated by See [Context.createDataStore] for more info. The user is
+     * responsible for ensuring that there is never more than one DataStore acting on a file at a
+     * time.
+     *
+     * @param context the context from which we retrieve files directory.
+     * @param fileName the filename relative to Context.filesDir that DataStore acts on. The File is
+     * obtained by calling File(context.filesDir, fileName). No two instances of DataStore should
+     * act on the same file at the same time.
+     * @param serializer the serializer for the type that this DataStore acts on.
+     */
+    public constructor(context: Context, fileName: String, serializer: Serializer<T>) {
+        this.context = context
+        this.name = fileName
+        this.serializer = serializer
+    }
+
+    // Either produceFile or context & name must be set, but not both. This is enforced by the
+    // two constructors.
+    private var produceFile: Callable<File>? = null
+
+    private var context: Context? = null
+    private var name: String? = null
+
+    // Required. This is enforced by the constructors.
+    private var serializer: Serializer<T>? = null
+
+    // Optional
+    private var ioScheduler: Scheduler = Schedulers.io()
+    private var corruptionHandler: ReplaceFileCorruptionHandler<T>? = null
+    private val dataMigrations: MutableList<DataMigration<T>> = mutableListOf()
+
+    /**
+     * Set the Scheduler on which to perform IO and transform operations. This is converted into
+     * a CoroutineDispatcher before being added to DataStore.
+     *
+     * This parameter is optional and defaults to Schedulers.io().
+     *
+     * @param ioScheduler the scheduler on which IO and transform operations are run
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun setIoScheduler(ioScheduler: Scheduler): RxDataStoreBuilder<T> =
+        apply { this.ioScheduler = ioScheduler }
+
+    /**
+     * Sets the corruption handler to install into the DataStore.
+     *
+     * This parameter is optional and defaults to no corruption handler.
+     *
+     * @param corruptionHandler
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun setCorruptionHandler(corruptionHandler: ReplaceFileCorruptionHandler<T>):
+        RxDataStoreBuilder<T> = apply { this.corruptionHandler = corruptionHandler }
+
+    /**
+     * Add an RxDataMigration to the DataStore. Migrations are run in the order they are added.
+     *
+     * @param rxDataMigration the migration to add.
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun addRxDataMigration(rxDataMigration: RxDataMigration<T>): RxDataStoreBuilder<T> =
+        apply {
+            this.dataMigrations.add(DataMigrationFromRxDataMigration(rxDataMigration))
+        }
+
+    /**
+     * Add a DataMigration to the Datastore. Migrations are run in the order they are added.
+     *
+     * @param dataMigration the migration to add
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun addDataMigration(dataMigration: DataMigration<T>): RxDataStoreBuilder<T> = apply {
+        this.dataMigrations.add(dataMigration)
+    }
+
+    /**
+     * Build the DataStore.
+     *
+     * @return the DataStore with the provided parameters
+     */
+    public fun build(): DataStore<T> {
+        val scope = CoroutineScope(ioScheduler.asCoroutineDispatcher())
+
+        return if (produceFile != null) {
+            DataStoreFactory.create(
+                produceFile = { produceFile!!.call() },
+                serializer = serializer!!,
+                scope = CoroutineScope(
+                    ioScheduler.asCoroutineDispatcher()
+                ),
+                corruptionHandler = corruptionHandler,
+                migrations = dataMigrations
+            )
+        } else if (context != null && name != null) {
+            return context!!.createDataStore(
+                fileName = name!!,
+                serializer = serializer!!,
+                scope = scope,
+                corruptionHandler = corruptionHandler,
+                migrations = dataMigrations
+            )
+        } else {
+            error(
+                "Either produceFile or context and name must be set. This should never happen."
+            )
+        }
+    }
+}
+
+internal class DataMigrationFromRxDataMigration<T>(private val migration: RxDataMigration<T>) :
+    DataMigration<T> {
+    override suspend fun shouldMigrate(currentData: T): Boolean {
+        return migration.shouldMigrate(currentData).await()
+    }
+
+    override suspend fun migrate(currentData: T): T {
+        return migration.migrate(currentData).await()
+    }
+
+    override suspend fun cleanUp() {
+        migration.cleanUp().await()
+    }
+}
diff --git a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
new file mode 100644
index 0000000..e5f71c6
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2020 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.datastore.rxjava3
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.datastore.core.DataMigration
+import androidx.datastore.migrations.SharedPreferencesMigration
+import androidx.datastore.migrations.SharedPreferencesView
+import io.reactivex.rxjava3.core.Single
+import kotlinx.coroutines.rx3.await
+
+/**
+ * Client implemented migration interface.
+ **/
+public interface RxSharedPreferencesMigration<T> {
+    /**
+     * Whether or not the migration should be run. This can be used to skip a read from the
+     * SharedPreferences.
+     *
+     * @param currentData the most recently persisted data
+     * @return a Single indicating whether or not the migration should be run.
+     */
+    public fun shouldMigrate(currentData: T): Single<Boolean> {
+        return Single.just(true)
+    }
+
+    /**
+     * Maps SharedPreferences into T. Implementations should be idempotent
+     * since this may be called multiple times. See [DataMigration.migrate] for more
+     * information. The method accepts a SharedPreferencesView which is the view of the
+     * SharedPreferences to migrate from (limited to [keysToMigrate] and a T which represent
+     * the current data. The function must return the migrated data.
+     *
+     * @param sharedPreferencesView the current state of the SharedPreferences
+     * @param currentData the most recently persisted data
+     * @return a Single of the updated data
+     */
+    public fun migrate(sharedPreferencesView: SharedPreferencesView, currentData: T): Single<T>
+}
+
+/**
+ * RxSharedPreferencesMigrationBuilder for the RxSharedPreferencesMigration.
+ */
+@SuppressLint("TopLevelBuilder")
+public class RxSharedPreferencesMigrationBuilder<T>
+/**
+ * Construct a RxSharedPreferencesMigrationBuilder.
+ *
+ * @param context the Context used for getting the SharedPreferences.
+ * @param sharedPreferencesName the name of the SharedPreference from which to migrate.
+ * @param rxSharedPreferencesMigration the user implemented migration for this SharedPreference.
+ */
+constructor(
+    private val context: Context,
+    private val sharedPreferencesName: String,
+    private val rxSharedPreferencesMigration: RxSharedPreferencesMigration<T>
+) {
+
+    /** Optional */
+    private var deleteEmptyPreference: Boolean = true
+    private var keysToMigrate: Set<String>? = null
+
+    /**
+     * Set the list of keys to migrate. The keys will be mapped to datastore.Preferences with
+     * their same values. If the key is already present in the new Preferences, the key
+     * will not be migrated again. If the key is not present in the SharedPreferences it
+     * will not be migrated.
+     *
+     * This method is optional and if keysToMigrate is not set, all keys will be migrated from the
+     * existing SharedPreferences.
+     *
+     * @param keys the keys to migrate
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun setKeysToMigrate(vararg keys: String):
+        RxSharedPreferencesMigrationBuilder<T> = apply {
+            keysToMigrate = setOf(*keys)
+        }
+
+    /**
+     * If enabled and the SharedPreferences are empty (i.e. no remaining
+     * keys) after this migration runs, the leftover SharedPreferences file is deleted. Note that
+     * this cleanup runs only if the migration itself runs, i.e., if the keys were never in
+     * SharedPreferences to begin with then the (potentially) empty SharedPreferences
+     * won't be cleaned up by this option. This functionality is best effort - if there
+     * is an issue deleting the SharedPreferences file it will be silently ignored.
+     *
+     * This method is optional and defaults to true.
+     *
+     * @param deleteEmptyPreferences whether or not to delete the empty shared preferences file
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun setDeleteEmptyPreferences(deleteEmptyPreferences: Boolean):
+        RxSharedPreferencesMigrationBuilder<T> = apply {
+            this.deleteEmptyPreference = deleteEmptyPreferences
+        }
+
+    public fun build(): DataMigration<T> {
+        return SharedPreferencesMigration(
+            context = context,
+            sharedPreferencesName = sharedPreferencesName,
+            migrate = { spView, curData ->
+                rxSharedPreferencesMigration.migrate(spView, curData).await()
+            },
+            keysToMigrate = keysToMigrate,
+            deleteEmptyPreferences = deleteEmptyPreference,
+            shouldRunMigration = { curData ->
+                rxSharedPreferencesMigration.shouldMigrate(curData).await()
+            }
+        )
+    }
+}
diff --git a/datastore/datastore-rxjava3/src/test-common/java/androidx/datastore/rxjava3/TestingSerializer.kt b/datastore/datastore-rxjava3/src/test-common/java/androidx/datastore/rxjava3/TestingSerializer.kt
new file mode 100644
index 0000000..f1e02bb
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/test-common/java/androidx/datastore/rxjava3/TestingSerializer.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 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.datastore.rxjava3
+
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.Serializer
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+
+class TestingSerializer(
+    @Volatile var failReadWithCorruptionException: Boolean = false,
+    @Volatile var failingRead: Boolean = false,
+    @Volatile var failingWrite: Boolean = false
+) : Serializer<Byte> {
+    override fun readFrom(input: InputStream): Byte {
+        if (failReadWithCorruptionException) {
+            throw CorruptionException(
+                "CorruptionException",
+                IOException()
+            )
+        }
+
+        if (failingRead) {
+            throw IOException("I was asked to fail on reads")
+        }
+
+        val read = input.read()
+        if (read == -1) {
+            return 0
+        }
+        return read.toByte()
+    }
+
+    override fun writeTo(t: Byte, output: OutputStream) {
+        if (failingWrite) {
+            throw IOException("I was asked to fail on writes")
+        }
+        output.write(t.toInt())
+    }
+
+    override val defaultValue: Byte = 0
+}
\ No newline at end of file
diff --git a/datastore/datastore-rxjava3/src/test/java/androidx/datastore/rxjava3/RxDataStoreTest.java b/datastore/datastore-rxjava3/src/test/java/androidx/datastore/rxjava3/RxDataStoreTest.java
new file mode 100644
index 0000000..83c05a8
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/test/java/androidx/datastore/rxjava3/RxDataStoreTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2020 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.datastore.rxjava3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.datastore.core.DataStore;
+import androidx.datastore.core.DataStoreFactory;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+import io.reactivex.rxjava3.core.Single;
+import io.reactivex.rxjava3.subscribers.TestSubscriber;
+import kotlinx.coroutines.CoroutineScopeKt;
+import kotlinx.coroutines.Dispatchers;
+
+public class RxDataStoreTest {
+    @Rule
+    public TemporaryFolder tempFolder = new TemporaryFolder();
+
+    private static Single<Byte> incrementByte(Byte byteIn) {
+        return Single.just(++byteIn);
+    }
+
+    @Test
+    public void testGetSingleValue() throws Exception {
+        File newFile = tempFolder.newFile();
+
+        DataStore<Byte> byteStore = DataStoreFactory.INSTANCE.create(
+                new TestingSerializer(),
+                null,
+                new ArrayList<>(),
+                CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()),
+                () -> newFile);
+
+        Byte firstByte = RxDataStore.data(byteStore).blockingFirst();
+        assertThat(firstByte).isEqualTo(0);
+
+        Single<Byte> incrementByte = RxDataStore.updateDataAsync(byteStore,
+                RxDataStoreTest::incrementByte);
+
+        assertThat(incrementByte.blockingGet()).isEqualTo(1);
+
+        firstByte = RxDataStore.data(byteStore).blockingFirst();
+        assertThat(firstByte).isEqualTo(1);
+    }
+
+    @Test
+    public void testTake3() throws Exception {
+        File newFile = tempFolder.newFile();
+
+        DataStore<Byte> byteStore = DataStoreFactory.INSTANCE.create(
+                new TestingSerializer(),
+                null,
+                new ArrayList<>(),
+                CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()),
+                () -> newFile);
+
+        TestSubscriber<Byte> testSubscriber = RxDataStore.data(byteStore).test();
+
+        RxDataStore.updateDataAsync(byteStore, RxDataStoreTest::incrementByte);
+        RxDataStore.updateDataAsync(byteStore, RxDataStoreTest::incrementByte);
+
+        testSubscriber.awaitCount(3).assertValues((byte) 0, (byte) 1, (byte) 2);
+    }
+
+
+    @Test
+    public void testReadFailure() throws Exception {
+        File newFile = tempFolder.newFile();
+        TestingSerializer testingSerializer = new TestingSerializer();
+
+        DataStore<Byte> byteStore = DataStoreFactory.INSTANCE.create(
+                testingSerializer,
+                null,
+                new ArrayList<>(),
+                CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()),
+                () -> newFile);
+
+        testingSerializer.setFailingRead(true);
+
+        TestSubscriber<Byte> testSubscriber = RxDataStore.data(byteStore).test();
+
+        assertThat(testSubscriber.await(5, TimeUnit.SECONDS)).isTrue();
+
+        testSubscriber.assertError(IOException.class);
+
+        testingSerializer.setFailingRead(false);
+
+        testSubscriber = RxDataStore.data(byteStore).test();
+        testSubscriber.awaitCount(1).assertValues((byte) 0);
+    }
+
+    @Test
+    public void testWriteFailure() throws Exception {
+        File newFile = tempFolder.newFile();
+        TestingSerializer testingSerializer = new TestingSerializer();
+
+        DataStore<Byte> byteStore = DataStoreFactory.INSTANCE.create(
+                testingSerializer,
+                null,
+                new ArrayList<>(),
+                CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()),
+                () -> newFile);
+
+        TestSubscriber<Byte> testSubscriber = RxDataStore.data(byteStore).test();
+
+        testingSerializer.setFailingWrite(true);
+        Single<Byte> incrementByte = RxDataStore.updateDataAsync(byteStore,
+                RxDataStoreTest::incrementByte);
+
+        incrementByte.cache().test().await().assertError(IOException.class);
+
+        testSubscriber.awaitCount(1).assertNoErrors().assertValues((byte) 0);
+        testingSerializer.setFailingWrite(false);
+
+        Single<Byte> incrementByte2 = RxDataStore.updateDataAsync(byteStore,
+                RxDataStoreTest::incrementByte);
+        assertThat(incrementByte2.blockingGet()).isEqualTo((byte) 1);
+
+        testSubscriber.awaitCount(2).assertValues((byte) 0, (byte) 1);
+    }
+}
diff --git a/lifecycle/.idea/codeStyles/Project.xml b/lifecycle/.idea/codeStyles/Project.xml
new file mode 120000
index 0000000..b52b28c
--- /dev/null
+++ b/lifecycle/.idea/codeStyles/Project.xml
@@ -0,0 +1 @@
+../../../.idea/codeStyles/Project.xml
\ No newline at end of file
diff --git a/lifecycle/.idea/codeStyles/codeStyleConfig.xml b/lifecycle/.idea/codeStyles/codeStyleConfig.xml
new file mode 120000
index 0000000..19c4848
--- /dev/null
+++ b/lifecycle/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1 @@
+../../../.idea/codeStyles/codeStyleConfig.xml
\ No newline at end of file
diff --git a/lifecycle/.idea/copyright/AndroidCopyright.xml b/lifecycle/.idea/copyright/AndroidCopyright.xml
new file mode 120000
index 0000000..afbbd04
--- /dev/null
+++ b/lifecycle/.idea/copyright/AndroidCopyright.xml
@@ -0,0 +1 @@
+../../../.idea/copyright/AndroidCopyright.xml
\ No newline at end of file
diff --git a/lifecycle/.idea/copyright/profiles_settings.xml b/lifecycle/.idea/copyright/profiles_settings.xml
new file mode 120000
index 0000000..5996ccd
--- /dev/null
+++ b/lifecycle/.idea/copyright/profiles_settings.xml
@@ -0,0 +1 @@
+../../../.idea/copyright/profiles_settings.xml
\ No newline at end of file
diff --git a/lifecycle/.idea/inspectionProfiles/Project_Default.xml b/lifecycle/.idea/inspectionProfiles/Project_Default.xml
new file mode 120000
index 0000000..a7481f4
--- /dev/null
+++ b/lifecycle/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1 @@
+../../../.idea/inspectionProfiles/Project_Default.xml
\ No newline at end of file
diff --git a/lifecycle/.idea/scopes/Ignore_API_Files.xml b/lifecycle/.idea/scopes/Ignore_API_Files.xml
new file mode 120000
index 0000000..3361ee1
--- /dev/null
+++ b/lifecycle/.idea/scopes/Ignore_API_Files.xml
@@ -0,0 +1 @@
+../../../.idea/scopes/Ignore_API_Files.xml
\ No newline at end of file
diff --git a/lifecycle/.idea/scopes/buildSrc.xml b/lifecycle/.idea/scopes/buildSrc.xml
new file mode 120000
index 0000000..25b7d3b
--- /dev/null
+++ b/lifecycle/.idea/scopes/buildSrc.xml
@@ -0,0 +1 @@
+../../../.idea/scopes/buildSrc.xml
\ No newline at end of file
diff --git a/lifecycle/gradle b/lifecycle/gradle
new file mode 120000
index 0000000..1c936b3
--- /dev/null
+++ b/lifecycle/gradle
@@ -0,0 +1 @@
+../playground-common/gradle
\ No newline at end of file
diff --git a/lifecycle/gradle.properties b/lifecycle/gradle.properties
new file mode 120000
index 0000000..d952fb0
--- /dev/null
+++ b/lifecycle/gradle.properties
@@ -0,0 +1 @@
+../playground-common/androidx-shared.properties
\ No newline at end of file
diff --git a/lifecycle/gradlew b/lifecycle/gradlew
new file mode 120000
index 0000000..05b75179
--- /dev/null
+++ b/lifecycle/gradlew
@@ -0,0 +1 @@
+../playground-common/gradlew
\ No newline at end of file
diff --git a/lifecycle/gradlew.bat b/lifecycle/gradlew.bat
new file mode 120000
index 0000000..b20877e
--- /dev/null
+++ b/lifecycle/gradlew.bat
@@ -0,0 +1 @@
+../playground-common/gradlew.bat
\ No newline at end of file
diff --git a/lifecycle/integration-tests/kotlintestapp/build.gradle b/lifecycle/integration-tests/kotlintestapp/build.gradle
index 432adec..e786607 100644
--- a/lifecycle/integration-tests/kotlintestapp/build.gradle
+++ b/lifecycle/integration-tests/kotlintestapp/build.gradle
@@ -28,7 +28,7 @@
 
 dependencies {
     implementation(project(":lifecycle:lifecycle-runtime-ktx"))
-    implementation(project(":activity:activity")) {
+    implementation(projectOrArtifact(":activity:activity")) {
         exclude group: 'androidx.lifecycle'
     }
     implementation(project(":lifecycle:lifecycle-viewmodel-savedstate")) {
diff --git a/lifecycle/integration-tests/testapp/build.gradle b/lifecycle/integration-tests/testapp/build.gradle
index 975f3ec..d231187 100644
--- a/lifecycle/integration-tests/testapp/build.gradle
+++ b/lifecycle/integration-tests/testapp/build.gradle
@@ -24,9 +24,12 @@
 
 dependencies {
     implementation(KOTLIN_STDLIB)
-    implementation(project(":fragment:fragment"))
+    implementation(projectOrArtifact(":fragment:fragment")) {
+        exclude group: "androidx.lifecycle", module: "lifecycle-runtime"
+    }
     implementation(project(":lifecycle:lifecycle-process"))
     implementation(project(":lifecycle:lifecycle-common"))
+    implementation(project(":lifecycle:lifecycle-runtime"))
     annotationProcessor(project(":lifecycle:lifecycle-compiler"))
 
     androidTestAnnotationProcessor(project(":lifecycle:lifecycle-compiler"))
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
index 7969a7c..4a710c9 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
@@ -38,7 +38,11 @@
     api(project(":lifecycle:lifecycle-livedata-core"))
     api(project(":lifecycle:lifecycle-viewmodel"))
 
-    androidTestImplementation project(":fragment:fragment"), {
+    androidTestImplementation project(":lifecycle:lifecycle-runtime")
+    androidTestImplementation project(":lifecycle:lifecycle-livedata-core")
+    androidTestImplementation projectOrArtifact(":fragment:fragment"), {
+        exclude group: 'androidx.lifecycle', module: 'lifecycle-runtime'
+        exclude group: 'androidx.lifecycle', module: 'lifecycle-livedata-core'
         exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-savedstate'
         exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
     }
diff --git a/lifecycle/settings.gradle b/lifecycle/settings.gradle
new file mode 100644
index 0000000..a451397
--- /dev/null
+++ b/lifecycle/settings.gradle
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 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.
+ */
+
+// see ../playground-common/README.md for details on how this works
+rootProject.name = "navigation-playground"
+apply from: "../playground-common/playground-include-settings.gradle"
+setupPlayground(this, "..")
+selectProjectsFromAndroidX({ name ->
+    if (name.startsWith(":lifecycle")) return true
+    if (name == ":annotation:annotation") return true
+    if (name == ":internal-testutils-runtime") return true
+    if (name == ":internal-testutils-truth") return true
+    return false
+})
+
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
index e7de321..b167251 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
@@ -463,12 +463,9 @@
         checkCallingThread();
 
         // Choose the fallback route if it's not already selected.
-        // Otherwise, select the default route.
         RouteInfo fallbackRoute = sGlobal.chooseFallbackRoute();
         if (sGlobal.getSelectedRoute() != fallbackRoute) {
             sGlobal.selectRoute(fallbackRoute, reason);
-        } else {
-            sGlobal.selectRoute(sGlobal.getDefaultRoute(), reason);
         }
     }
 
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java
index 8554527..3033b44 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java
@@ -63,16 +63,16 @@
     @Before
     public void createDb() throws TimeoutException, InterruptedException {
         Context context = ApplicationProvider.getApplicationContext();
+        context.deleteDatabase("testDb");
         mDb = Room.databaseBuilder(context, TestDatabase.class, "testDb")
                 .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
                 .addCallback(mCallback).build();
         mUserDao = mDb.getUserDao();
-        drain();
     }
 
     @After
     public void cleanUp() throws Exception {
-        mDb.clearAllTables();
+        drain();
         mDb.close();
     }
 
@@ -124,11 +124,9 @@
     @Test
     @MediumTest
     public void slowCursorClosing_keepsDbAlive() throws Exception {
-        assertFalse(mCallback.mOpened);
         User user = TestUtil.createUser(1);
         user.setName("bob");
         mUserDao.insert(user);
-        assertTrue(mCallback.mOpened);
         mUserDao.load(1);
 
         Cursor cursor = mDb.query("select * from user", null);
@@ -194,20 +192,22 @@
     @MediumTest
     public void testCanExecSqlInCallback() throws Exception {
         Context context = ApplicationProvider.getApplicationContext();
-
-        mDb = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+        context.deleteDatabase("testDb2");
+        TestDatabase db = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
                         .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
                         .addCallback(new ExecSqlInCallback())
                         .build();
 
-        mDb.getUserDao().insert(TestUtil.createUser(1));
+        db.getUserDao().insert(TestUtil.createUser(1));
+
+        db.close();
     }
 
     @Test
     public void testManuallyRoomDatabaseClose() throws Exception {
         Context context = ApplicationProvider.getApplicationContext();
         // Create a new db since the other one is cleared in the @After
-        TestDatabase testDatabase = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+        TestDatabase testDatabase = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
                 .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
                 .addCallback(new ExecSqlInCallback())
                 .build();
@@ -222,7 +222,7 @@
         assertFalse(testDatabase.isOpen());
 
         assertFalse(testDatabase.isOpen());
-        TestDatabase testDatabase2 = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+        TestDatabase testDatabase2 = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
                 .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
                 .addCallback(new ExecSqlInCallback())
                 .build();
@@ -235,7 +235,7 @@
     public void testManuallyOpenHelperClose() throws Exception {
         Context context = ApplicationProvider.getApplicationContext();
         // Create a new db since the other one is cleared in the @After
-        TestDatabase testDatabase = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+        TestDatabase testDatabase = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
                 .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
                 .addCallback(new ExecSqlInCallback())
                 .build();
@@ -248,7 +248,7 @@
         }).hasMessageThat().contains("closed");
 
         assertFalse(testDatabase.isOpen());
-        TestDatabase testDatabase2 = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+        TestDatabase testDatabase2 = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
                 .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
                 .addCallback(new ExecSqlInCallback())
                 .build();
@@ -294,8 +294,8 @@
     public void invalidationObserver_canRequeryDb() throws TimeoutException, InterruptedException {
         Context context = ApplicationProvider.getApplicationContext();
 
-        context.deleteDatabase("testDb");
-        mDb = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+        context.deleteDatabase("testDb2");
+        TestDatabase db = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
                 // create contention for callback
                 .setAutoCloseTimeout(0, TimeUnit.MILLISECONDS)
                 .addCallback(mCallback).build();
@@ -303,20 +303,21 @@
         AtomicInteger userCount = new AtomicInteger(0);
 
         UserTableObserver userTableObserver = new UserTableObserver(
-                () -> userCount.set(mUserDao.count()));
+                () -> userCount.set(db.getUserDao().count()));
 
-        mDb.getInvalidationTracker().addObserver(userTableObserver);
+        db.getInvalidationTracker().addObserver(userTableObserver);
 
-        mDb.getUserDao().insert(TestUtil.createUser(1));
-        mDb.getUserDao().insert(TestUtil.createUser(2));
-        mDb.getUserDao().insert(TestUtil.createUser(3));
-        mDb.getUserDao().insert(TestUtil.createUser(4));
-        mDb.getUserDao().insert(TestUtil.createUser(5));
-        mDb.getUserDao().insert(TestUtil.createUser(6));
-        mDb.getUserDao().insert(TestUtil.createUser(7));
+        db.getUserDao().insert(TestUtil.createUser(1));
+        db.getUserDao().insert(TestUtil.createUser(2));
+        db.getUserDao().insert(TestUtil.createUser(3));
+        db.getUserDao().insert(TestUtil.createUser(4));
+        db.getUserDao().insert(TestUtil.createUser(5));
+        db.getUserDao().insert(TestUtil.createUser(6));
+        db.getUserDao().insert(TestUtil.createUser(7));
 
         drain();
         assertEquals(7, userCount.get());
+        db.close();
     }
 
     @Test
@@ -325,8 +326,8 @@
             InterruptedException {
         Context context = ApplicationProvider.getApplicationContext();
 
-        context.deleteDatabase("testDb");
-        mDb = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+        context.deleteDatabase("testDb2");
+        TestDatabase db = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
                 // create contention for callback
                 .setAutoCloseTimeout(0, TimeUnit.MILLISECONDS)
                 .addCallback(mCallback).build();
@@ -336,21 +337,22 @@
         UserTableObserver userTableObserver =
                 new UserTableObserver(invalidationCount::getAndIncrement);
 
-        mDb.getInvalidationTracker().addObserver(userTableObserver);
+        db.getInvalidationTracker().addObserver(userTableObserver);
 
 
-        mDb.getUserDao().insert(TestUtil.createUser(1));
+        db.getUserDao().insert(TestUtil.createUser(1));
 
         drain();
         assertEquals(1, invalidationCount.get());
 
         Thread.sleep(100); // Let db auto close
 
-        mDb.getInvalidationTracker().notifyObserversByTableNames("user");
+        db.getInvalidationTracker().notifyObserversByTableNames("user");
 
         drain();
         assertEquals(2, invalidationCount.get());
 
+        db.close();
     }
 
     private void drain() throws TimeoutException, InterruptedException {
diff --git a/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt b/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
index bee72dd..1a6147b 100644
--- a/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
+++ b/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
@@ -23,7 +23,7 @@
 import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
+import androidx.test.filters.MediumTest
 import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
@@ -35,7 +35,7 @@
 import java.util.concurrent.TimeUnit
 
 @RunWith(AndroidJUnit4::class)
-@SmallTest
+@MediumTest
 public class AutoCloserTest {
 
     @get:Rule
diff --git a/settings.gradle b/settings.gradle
index 07e33f3..72c87d0 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -285,8 +285,13 @@
         [BuildType.MAIN])
 includeProject(":datastore:datastore-preferences-core:datastore-preferences-proto",
         "datastore/datastore-preferences-core/datastore-preferences-proto", [BuildType.MAIN])
+includeProject(":datastore:datastore-preferences-rxjava2",
+        "datastore/datastore-preferences-rxjava2", [BuildType.MAIN])
+includeProject(":datastore:datastore-preferences-rxjava3",
+        "datastore/datastore-preferences-rxjava3", [BuildType.MAIN])
 includeProject(":datastore:datastore-proto", "datastore/datastore-proto", [BuildType.MAIN])
 includeProject(":datastore:datastore-rxjava2", "datastore/datastore-rxjava2", [BuildType.MAIN])
+includeProject(":datastore:datastore-rxjava3", "datastore/datastore-rxjava3", [BuildType.MAIN])
 includeProject(":datastore:datastore-sampleapp", "datastore/datastore-sampleapp", [BuildType.MAIN])
 includeProject(":documentfile:documentfile", "documentfile/documentfile", [BuildType.MAIN])
 includeProject(":drawerlayout:drawerlayout", "drawerlayout/drawerlayout", [BuildType.MAIN])
diff --git a/wear/wear-watchface-client/api/current.txt b/wear/wear-watchface-client/api/current.txt
index c744f24..1cdcbe1 100644
--- a/wear/wear-watchface-client/api/current.txt
+++ b/wear/wear-watchface-client/api/current.txt
@@ -57,7 +57,7 @@
     method public long getPreviewReferenceTimeMillis();
     method public void performAmbientTick();
     method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
-    method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
+    method public void setSystemState(androidx.wear.watchface.client.SystemState systemState);
     method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
     property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/api/public_plus_experimental_current.txt b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
index 3e7223f..ea7f3e7 100644
--- a/wear/wear-watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
@@ -57,7 +57,7 @@
     method public long getPreviewReferenceTimeMillis();
     method public void performAmbientTick();
     method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
-    method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
+    method public void setSystemState(androidx.wear.watchface.client.SystemState systemState);
     method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
     property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/api/restricted_current.txt b/wear/wear-watchface-client/api/restricted_current.txt
index 7961108..0ef5468 100644
--- a/wear/wear-watchface-client/api/restricted_current.txt
+++ b/wear/wear-watchface-client/api/restricted_current.txt
@@ -57,7 +57,7 @@
     method public long getPreviewReferenceTimeMillis();
     method public void performAmbientTick();
     method public void sendTouchEvent(int xPosition, int yPosition, @androidx.wear.watchface.client.TapType int tapType);
-    method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
+    method public void setSystemState(androidx.wear.watchface.client.SystemState systemState);
     method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
     property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
index 5d41c43..f8b63aa 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
@@ -32,7 +32,6 @@
 import androidx.wear.watchface.control.IInteractiveWatchFaceSysUI
 import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
-import androidx.wear.watchface.data.SystemState
 import androidx.wear.watchface.style.UserStyle
 import java.util.Objects
 
@@ -219,7 +218,7 @@
 
     override fun setSystemState(systemState: SystemState) {
         iInteractiveWatchFaceSysUI.setSystemState(
-            SystemState(
+            androidx.wear.watchface.data.SystemState(
                 systemState.inAmbientMode,
                 systemState.interruptionFilter
             )
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
index ad56258..9df8117 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
@@ -108,6 +108,8 @@
 
     private val styleListeners = HashSet<UserStyleListener>()
 
+    private val idToStyleSetting = schema.userStyleSettings.associateBy { it.id }
+
     /**
      * The current [UserStyle]. Assigning to this property triggers immediate [UserStyleListener]
      * callbacks if if any options have changed.
@@ -128,11 +130,12 @@
                 field.selectedOptions as HashMap<UserStyleSetting, UserStyleSetting.Option>
             for ((setting, option) in style.selectedOptions) {
                 // Ignore an unrecognized setting.
-                val styleSetting = field.selectedOptions[setting] ?: continue
+                val localSetting = idToStyleSetting[setting.id] ?: continue
+                val styleSetting = field.selectedOptions[localSetting] ?: continue
                 if (styleSetting.id != option.id) {
                     changed = true
                 }
-                hashmap[setting] = option
+                hashmap[localSetting] = option
             }
 
             if (!changed) {
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt
index e8cc5b6..c4c338f 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt
@@ -122,7 +122,6 @@
         Mockito.verify(mockListener3).onUserStyleChanged(userStyleRepository.userStyle)
     }
 
-    @Test
     fun assigning_userStyle() {
         val newStyle = UserStyle(
             hashMapOf(
@@ -140,6 +139,40 @@
     }
 
     @Test
+    fun assign_userStyle_with_distinctButMatchingRefs() {
+        val colorStyleSetting2 = ListUserStyleSetting(
+            "color_style_setting",
+            "Colors",
+            "Watchface colorization", /* icon = */
+            null,
+            colorStyleList,
+            listOf(Layer.BASE_LAYER)
+        )
+        val watchHandStyleSetting2 = ListUserStyleSetting(
+            "hand_style_setting",
+            "Hand Style",
+            "Hand visual look", /* icon = */
+            null,
+            watchHandStyleList,
+            listOf(Layer.TOP_LAYER)
+        )
+
+        val newStyle = UserStyle(
+            hashMapOf(
+                colorStyleSetting2 to greenStyleOption,
+                watchHandStyleSetting2 to gothicStyleOption
+            )
+        )
+
+        userStyleRepository.userStyle = newStyle
+
+        assertThat(userStyleRepository.userStyle.selectedOptions[colorStyleSetting])
+            .isEqualTo(greenStyleOption)
+        assertThat(userStyleRepository.userStyle.selectedOptions[watchHandStyleSetting])
+            .isEqualTo(gothicStyleOption)
+    }
+
+    @Test
     fun defaultValues() {
         val watchHandLengthOption =
             userStyleRepository.userStyle.selectedOptions[watchHandLengthStyleSetting]!! as
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index 37fb061..b3a90c4 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -59,6 +59,7 @@
   public static final class Complication.Builder {
     method public androidx.wear.watchface.Complication build();
     method public androidx.wear.watchface.Complication.Builder setDefaultProviderType(androidx.wear.complications.data.ComplicationType defaultProviderType);
+    method public androidx.wear.watchface.Complication.Builder setEnabled(boolean enabled);
   }
 
   public static final class Complication.Companion {
@@ -242,13 +243,14 @@
   }
 
   public final class WatchState {
-    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis, boolean isHeadless);
     method public long getAnalogPreviewReferenceTimeMillis();
     method public long getDigitalPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
     method public boolean hasBurnInProtection();
     method public boolean hasLowBitAmbient();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
+    method public boolean isHeadless();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible();
     property public final long analogPreviewReferenceTimeMillis;
     property public final long digitalPreviewReferenceTimeMillis;
@@ -256,6 +258,7 @@
     property public final boolean hasLowBitAmbient;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
+    property public final boolean isHeadless;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
   }
 
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index 37fb061..b3a90c4 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -59,6 +59,7 @@
   public static final class Complication.Builder {
     method public androidx.wear.watchface.Complication build();
     method public androidx.wear.watchface.Complication.Builder setDefaultProviderType(androidx.wear.complications.data.ComplicationType defaultProviderType);
+    method public androidx.wear.watchface.Complication.Builder setEnabled(boolean enabled);
   }
 
   public static final class Complication.Companion {
@@ -242,13 +243,14 @@
   }
 
   public final class WatchState {
-    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis, boolean isHeadless);
     method public long getAnalogPreviewReferenceTimeMillis();
     method public long getDigitalPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
     method public boolean hasBurnInProtection();
     method public boolean hasLowBitAmbient();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
+    method public boolean isHeadless();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible();
     property public final long analogPreviewReferenceTimeMillis;
     property public final long digitalPreviewReferenceTimeMillis;
@@ -256,6 +258,7 @@
     property public final boolean hasLowBitAmbient;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
+    property public final boolean isHeadless;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
   }
 
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index a4ad2dc..cc848dc 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -59,6 +59,7 @@
   public static final class Complication.Builder {
     method public androidx.wear.watchface.Complication build();
     method public androidx.wear.watchface.Complication.Builder setDefaultProviderType(androidx.wear.complications.data.ComplicationType defaultProviderType);
+    method public androidx.wear.watchface.Complication.Builder setEnabled(boolean enabled);
   }
 
   public static final class Complication.Companion {
@@ -133,11 +134,13 @@
     method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Integer> getInterruptionFilter();
     method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isAmbient();
     method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging();
+    method public boolean isHeadless();
     method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isVisible();
     method public void setAnalogPreviewReferenceTimeMillis(long p);
     method public void setDigitalPreviewReferenceTimeMillis(long p);
     method public void setHasBurnInProtection(boolean p);
     method public void setHasLowBitAmbient(boolean p);
+    method public void setHeadless(boolean p);
     method public void setInterruptionFilter(androidx.wear.watchface.MutableObservableWatchData<java.lang.Integer> p);
     property public final long analogPreviewReferenceTimeMillis;
     property public final long digitalPreviewReferenceTimeMillis;
@@ -146,6 +149,7 @@
     property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Integer> interruptionFilter;
     property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isAmbient;
     property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging;
+    property public final boolean isHeadless;
     property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isVisible;
   }
 
@@ -287,13 +291,14 @@
   }
 
   public final class WatchState {
-    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis, boolean isHeadless);
     method public long getAnalogPreviewReferenceTimeMillis();
     method public long getDigitalPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
     method public boolean hasBurnInProtection();
     method public boolean hasLowBitAmbient();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
+    method public boolean isHeadless();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible();
     property public final long analogPreviewReferenceTimeMillis;
     property public final long digitalPreviewReferenceTimeMillis;
@@ -301,6 +306,7 @@
     property public final boolean hasLowBitAmbient;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
+    property public final boolean isHeadless;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
   }
 
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
index 2074339..ab40b74 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
@@ -244,7 +244,12 @@
     canvasComplication: CanvasComplication,
     supportedTypes: List<ComplicationType>,
     defaultProviderPolicy: DefaultComplicationProviderPolicy,
-    defaultProviderType: ComplicationType
+    defaultProviderType: ComplicationType,
+    /**
+     * The initial state of the complication. Note complications can be enabled / disabled by
+     * [UserStyleSetting.ComplicationsUserStyleSetting].
+     */
+    initiallyEnabled: Boolean
 ) {
     public companion object {
         internal val unitSquare = RectF(0f, 0f, 1f, 1f)
@@ -346,6 +351,7 @@
         private val complicationBounds: ComplicationBounds
     ) {
         private var defaultProviderType = ComplicationType.NOT_CONFIGURED
+        private var initiallyEnabled = true
 
         /**
          * Sets the initial [ComplicationType] to use with the initial complication provider.
@@ -359,6 +365,11 @@
             return this
         }
 
+        public fun setEnabled(enabled: Boolean): Builder {
+            this.initiallyEnabled = enabled
+            return this
+        }
+
         /** Constructs the [Complication]. */
         public fun build(): Complication = Complication(
             id,
@@ -367,7 +378,8 @@
             renderer,
             supportedTypes,
             defaultProviderPolicy,
-            defaultProviderType
+            defaultProviderType,
+            initiallyEnabled
         )
     }
 
@@ -412,7 +424,7 @@
     internal var enabledDirty = true
 
     /** Whether or not the complication should be drawn and accept taps. */
-    public var enabled: Boolean = true
+    public var enabled: Boolean = initiallyEnabled
         @JvmName("isEnabled")
         @UiThread
         get
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index bd3728c..97a6926 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -490,39 +490,41 @@
             }
         )
 
-        WatchFaceConfigActivity.registerWatchFace(
-            componentName,
-            object : WatchFaceConfigDelegate {
-                override fun getUserStyleSchema() = userStyleRepository.schema.toWireFormat()
+        if (!watchState.isHeadless) {
+            WatchFaceConfigActivity.registerWatchFace(
+                componentName,
+                object : WatchFaceConfigDelegate {
+                    override fun getUserStyleSchema() = userStyleRepository.schema.toWireFormat()
 
-                override fun getUserStyle() = userStyleRepository.userStyle.toWireFormat()
+                    override fun getUserStyle() = userStyleRepository.userStyle.toWireFormat()
 
-                override fun setUserStyle(userStyle: UserStyleWireFormat) {
-                    userStyleRepository.userStyle =
-                        UserStyle(userStyle, userStyleRepository.schema)
+                    override fun setUserStyle(userStyle: UserStyleWireFormat) {
+                        userStyleRepository.userStyle =
+                            UserStyle(userStyle, userStyleRepository.schema)
+                    }
+
+                    override fun getBackgroundComplicationId() =
+                        complicationsManager.getBackgroundComplication()?.id
+
+                    override fun getComplicationsMap() = complicationsManager.complications
+
+                    override fun getCalendar() = calendar
+
+                    override fun getComplicationIdAt(tapX: Int, tapY: Int) =
+                        complicationsManager.getComplicationAt(tapX, tapY)?.id
+
+                    override fun brieflyHighlightComplicationId(complicationId: Int) {
+                        complicationsManager.bringAttentionToComplication(complicationId)
+                    }
+
+                    override fun takeScreenshot(
+                        drawRect: Rect,
+                        calendar: Calendar,
+                        renderParameters: RenderParametersWireFormat
+                    ) = renderer.takeScreenshot(calendar, RenderParameters(renderParameters))
                 }
-
-                override fun getBackgroundComplicationId() =
-                    complicationsManager.getBackgroundComplication()?.id
-
-                override fun getComplicationsMap() = complicationsManager.complications
-
-                override fun getCalendar() = calendar
-
-                override fun getComplicationIdAt(tapX: Int, tapY: Int) =
-                    complicationsManager.getComplicationAt(tapX, tapY)?.id
-
-                override fun brieflyHighlightComplicationId(complicationId: Int) {
-                    complicationsManager.bringAttentionToComplication(complicationId)
-                }
-
-                override fun takeScreenshot(
-                    drawRect: Rect,
-                    calendar: Calendar,
-                    renderParameters: RenderParametersWireFormat
-                ) = renderer.takeScreenshot(calendar, RenderParameters(renderParameters))
-            }
-        )
+            )
+        }
 
         watchState.isAmbient.addObserver(ambientObserver)
         watchState.interruptionFilter.addObserver(interruptionFilterObserver)
@@ -549,7 +551,9 @@
         watchState.isAmbient.removeObserver(ambientObserver)
         watchState.interruptionFilter.removeObserver(interruptionFilterObserver)
         watchState.isVisible.removeObserver(visibilityObserver)
-        WatchFaceConfigActivity.unregisterWatchFace(componentName)
+        if (!watchState.isHeadless) {
+            WatchFaceConfigActivity.unregisterWatchFace(componentName)
+        }
         unregisterReceivers()
     }
 
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index c1334b1..3ed9356 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -801,6 +801,7 @@
                 }
             }
 
+            mutableWatchState.isHeadless = true
             val watchState = mutableWatchState.asWatchState()
             watchFaceImpl = WatchFaceImpl(
                 createWatchFace(fakeSurfaceHolder, watchState),
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
index 26df645..8b664c3 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
@@ -68,7 +68,10 @@
     public val analogPreviewReferenceTimeMillis: Long,
 
     /** UTC reference time for previews of digital watch faces in milliseconds since the epoch. */
-    public val digitalPreviewReferenceTimeMillis: Long
+    public val digitalPreviewReferenceTimeMillis: Long,
+
+    /** Whether or not this is a headless watchface. */
+    public val isHeadless: Boolean
 )
 
 /** @hide */
@@ -83,6 +86,7 @@
     public var hasBurnInProtection: Boolean = false
     public var analogPreviewReferenceTimeMillis: Long = 0
     public var digitalPreviewReferenceTimeMillis: Long = 0
+    public var isHeadless: Boolean = false
 
     public fun asWatchState(): WatchState = WatchState(
         interruptionFilter = interruptionFilter,
@@ -92,6 +96,7 @@
         hasLowBitAmbient = hasLowBitAmbient,
         hasBurnInProtection = hasBurnInProtection,
         analogPreviewReferenceTimeMillis = analogPreviewReferenceTimeMillis,
-        digitalPreviewReferenceTimeMillis = digitalPreviewReferenceTimeMillis
+        digitalPreviewReferenceTimeMillis = digitalPreviewReferenceTimeMillis,
+        isHeadless = isHeadless
     )
 }
\ No newline at end of file