[go: nahoru, domu]

Merge "Fix didExceedMaxLines test fails on API 21" into androidx-master-dev
diff --git a/.github/workflows/presubmit.yml b/.github/workflows/presubmit.yml
index d44d46c..3fd78ec 100644
--- a/.github/workflows/presubmit.yml
+++ b/.github/workflows/presubmit.yml
@@ -49,30 +49,28 @@
         with:
           fetch-depth: 1
 
-      - name: "Cache ~/.gradle/caches"
-        uses: actions/cache@v2.1.2
+      - name: "Setup JDK 11"
+        id: setup-java
+        uses: actions/setup-java@v1
         with:
-          path: "~/.gradle/caches"
-          key: gradle-cache-${{ runner.os }}-${{ hashFiles('**/*.gradle') }}-${{ hashFiles('**/gradle.properties') }}
-          restore-keys: |
-            gradle-cache-${{ runner.os }}-
+          java-version: "11"
 
-      - name: "Cache ~/.gradlew/wrapper"
-        uses: actions/cache@v2.1.2
-        with:
-          path: ~/.gradle/wrapper
-          key: gradle-wrapper-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
+      - 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: "Cache ~/.konan"
-        uses: actions/cache@v2.1.2
+      - name: "./gradlew buildOnServer"
+        uses: eskatos/gradle-command-action@v1
+        env:
+          JAVA_HOME: ${{ steps.setup-java.outputs.path }}
         with:
-          path: ~/.konan
-          key: konan-${{ runner.os }}
-
-      - name: "Build"
-        uses: androidx/build-on-server-action@main
-        with:
-          path: ${{ env.group-id }}
+          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
@@ -104,30 +102,28 @@
         with:
           fetch-depth: 1
 
-      - name: "Cache ~/.gradle/caches"
-        uses: actions/cache@v2.1.2
+      - name: "Setup JDK 11"
+        id: setup-java
+        uses: actions/setup-java@v1
         with:
-          path: "~/.gradle/caches"
-          key: gradle-cache-${{ runner.os }}-${{ hashFiles('**/*.gradle') }}-${{ hashFiles('**/gradle.properties') }}
-          restore-keys: |
-            gradle-cache-${{ runner.os }}-
+          java-version: "11"
 
-      - name: "Cache ~/.gradlew/wrapper"
-        uses: actions/cache@v2.1.2
-        with:
-          path: ~/.gradle/wrapper
-          key: gradle-wrapper-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
+      - 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: "Cache ~/.konan"
-        uses: actions/cache@v2.1.2
+      - name: "./gradlew buildOnServer"
+        uses: eskatos/gradle-command-action@v1
+        env:
+          JAVA_HOME: ${{ steps.setup-java.outputs.path }}
         with:
-          path: ~/.konan
-          key: konan-${{ runner.os }}
-
-      - name: "Build"
-        uses: androidx/build-on-server-action@main
-        with:
-          path: ${{ env.group-id }}
+          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
@@ -159,30 +155,28 @@
         with:
           fetch-depth: 1
 
-      - name: "Cache ~/.gradle/caches"
-        uses: actions/cache@v2.1.2
+      - name: "Setup JDK 11"
+        id: setup-java
+        uses: actions/setup-java@v1
         with:
-          path: "~/.gradle/caches"
-          key: gradle-cache-${{ runner.os }}-${{ hashFiles('**/*.gradle') }}-${{ hashFiles('**/gradle.properties') }}
-          restore-keys: |
-            gradle-cache-${{ runner.os }}-
+          java-version: "11"
 
-      - name: "Cache ~/.gradlew/wrapper"
-        uses: actions/cache@v2.1.2
-        with:
-          path: ~/.gradle/wrapper
-          key: gradle-wrapper-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
+      - 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: "Cache ~/.konan"
-        uses: actions/cache@v2.1.2
+      - name: "./gradlew buildOnServer"
+        uses: eskatos/gradle-command-action@v1
+        env:
+          JAVA_HOME: ${{ steps.setup-java.outputs.path }}
         with:
-          path: ~/.konan
-          key: konan-${{ runner.os }}
-
-      - name: "Build"
-        uses: androidx/build-on-server-action@main
-        with:
-          path: ${{ env.group-id }}
+          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
@@ -214,30 +208,28 @@
         with:
           fetch-depth: 1
 
-      - name: "Cache ~/.gradle/caches"
-        uses: actions/cache@v2.1.2
+      - name: "Setup JDK 11"
+        id: setup-java
+        uses: actions/setup-java@v1
         with:
-          path: "~/.gradle/caches"
-          key: gradle-cache-${{ runner.os }}-${{ hashFiles('**/*.gradle') }}-${{ hashFiles('**/gradle.properties') }}
-          restore-keys: |
-            gradle-cache-${{ runner.os }}-
+          java-version: "11"
 
-      - name: "Cache ~/.gradlew/wrapper"
-        uses: actions/cache@v2.1.2
-        with:
-          path: ~/.gradle/wrapper
-          key: gradle-wrapper-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
+      - 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: "Cache ~/.konan"
-        uses: actions/cache@v2.1.2
+      - name: "./gradlew buildOnServer"
+        uses: eskatos/gradle-command-action@v1
+        env:
+          JAVA_HOME: ${{ steps.setup-java.outputs.path }}
         with:
-          path: ~/.konan
-          key: konan-${{ runner.os }}
-
-      - name: "Build"
-        uses: androidx/build-on-server-action@main
-        with:
-          path: ${{ env.group-id }}
+          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
@@ -269,32 +261,31 @@
         with:
           fetch-depth: 1
 
-      - name: "Cache ~/.gradle/caches"
-        uses: actions/cache@v2.1.2
+      - name: "Setup JDK 11"
+        id: setup-java
+        uses: actions/setup-java@v1
         with:
-          path: "~/.gradle/caches"
-          key: gradle-cache-${{ runner.os }}-${{ hashFiles('**/*.gradle') }}-${{ hashFiles('**/gradle.properties') }}
-          restore-keys: |
-            gradle-cache-${{ runner.os }}-
+          java-version: "11"
 
-      - name: "Cache ~/.gradlew/wrapper"
-        uses: actions/cache@v2.1.2
+      - 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:
-          path: ~/.gradle/wrapper
-          key: gradle-wrapper-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
+          arguments: buildOnServer
+          build-root-directory: ${{ env.group-id }}
+          gradle-executable: ${{ env.group-id }}/gradlew
+          wrapper-directory: ${{ env.group-id }}/gradle/wrapper
 
-      - name: "Cache ~/.konan"
-        uses: actions/cache@v2.1.2
-        with:
-          path: ~/.konan
-          key: konan-${{ runner.os }}
 
-      - name: "Build"
-        uses: androidx/build-on-server-action@main
-        with:
-          path: ${{ env.group-id }}
-
-      - name: "Upload build artifacts"
+      - name: "upload build artifacts"
         continue-on-error: true
         if: always()
         uses: actions/upload-artifact@v2
@@ -324,30 +315,28 @@
         with:
           fetch-depth: 1
 
-      - name: "Cache ~/.gradle/caches"
-        uses: actions/cache@v2.1.2
+      - name: "Setup JDK 11"
+        id: setup-java
+        uses: actions/setup-java@v1
         with:
-          path: "~/.gradle/caches"
-          key: gradle-cache-${{ runner.os }}-${{ hashFiles('**/*.gradle') }}-${{ hashFiles('**/gradle.properties') }}
-          restore-keys: |
-            gradle-cache-${{ runner.os }}-
+          java-version: "11"
 
-      - name: "Cache ~/.gradlew/wrapper"
-        uses: actions/cache@v2.1.2
-        with:
-          path: ~/.gradle/wrapper
-          key: gradle-wrapper-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
+      - 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: "Cache ~/.konan"
-        uses: actions/cache@v2.1.2
+      - name: "./gradlew buildOnServer"
+        uses: eskatos/gradle-command-action@v1
+        env:
+          JAVA_HOME: ${{ steps.setup-java.outputs.path }}
         with:
-          path: ~/.konan
-          key: konan-${{ runner.os }}
-
-      - name: "Build"
-        uses: androidx/build-on-server-action@main
-        with:
-          path: ${{ env.group-id }}
+          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
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTest.java
index 080cf76..09ac4df 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTest.java
@@ -16,6 +16,8 @@
 
 package androidx.appsearch.app;
 
+import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
+import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
 import static androidx.appsearch.app.AppSearchTestUtils.checkIsBatchResultSuccess;
 import static androidx.appsearch.app.AppSearchTestUtils.checkIsResultSuccess;
 import static androidx.appsearch.app.AppSearchTestUtils.convertSearchResultsToDocuments;
@@ -51,6 +53,27 @@
     }
 
     @AppSearchDocument
+    static class Card {
+        @AppSearchDocument.Uri String mUri;
+        @AppSearchDocument.Property
+                (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
+        String mString;        // 3a
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof Card)) {
+                return false;
+            }
+            Card otherCard = (Card) other;
+            assertThat(otherCard.mUri).isEqualTo(this.mUri);
+            return true;
+        }
+    }
+
+    @AppSearchDocument
     static class Gift {
         @AppSearchDocument.Uri String mUri;
 
@@ -62,7 +85,7 @@
         @AppSearchDocument.Property Collection<Boolean> mCollectBoolean;   // 1a
         @AppSearchDocument.Property Collection<byte[]> mCollectByteArr;    // 1a
         @AppSearchDocument.Property Collection<String> mCollectString;     // 1b
-        @AppSearchDocument.Property Collection<Gift> mCollectGift;         // 1c
+        @AppSearchDocument.Property Collection<Card> mCollectCard;         // 1c
 
         // Arrays
         @AppSearchDocument.Property Long[] mArrBoxLong;         // 2a
@@ -78,7 +101,7 @@
         @AppSearchDocument.Property byte[][] mArrUnboxByteArr;  // 2b
         @AppSearchDocument.Property Byte[] mBoxByteArr;         // 2a
         @AppSearchDocument.Property String[] mArrString;        // 2b
-        @AppSearchDocument.Property Gift[] mArrGift;            // 2c
+        @AppSearchDocument.Property Card[] mArrCard;            // 2c
 
         // Single values
         @AppSearchDocument.Property String mString;        // 3a
@@ -93,7 +116,7 @@
         @AppSearchDocument.Property Boolean mBoxBoolean;   // 3a
         @AppSearchDocument.Property boolean mUnboxBoolean; // 3b
         @AppSearchDocument.Property byte[] mUnboxByteArr;  // 3a
-        @AppSearchDocument.Property Gift mGift;            // 3c
+        @AppSearchDocument.Property Card mCard;            // 3c
 
         @Override
         public boolean equals(Object other) {
@@ -118,7 +141,7 @@
             assertThat(otherGift.mArrUnboxFloat).isEqualTo(this.mArrUnboxFloat);
             assertThat(otherGift.mArrUnboxLong).isEqualTo(this.mArrUnboxLong);
             assertThat(otherGift.mArrUnboxInt).isEqualTo(this.mArrUnboxInt);
-            assertThat(otherGift.mArrGift).isEqualTo(this.mArrGift);
+            assertThat(otherGift.mArrCard).isEqualTo(this.mArrCard);
 
             assertThat(otherGift.mCollectLong).isEqualTo(this.mCollectLong);
             assertThat(otherGift.mCollectInteger).isEqualTo(this.mCollectInteger);
@@ -126,7 +149,7 @@
             assertThat(otherGift.mCollectString).isEqualTo(this.mCollectString);
             assertThat(otherGift.mCollectDouble).isEqualTo(this.mCollectDouble);
             assertThat(otherGift.mCollectFloat).isEqualTo(this.mCollectFloat);
-            assertThat(otherGift.mCollectGift).isEqualTo(this.mCollectGift);
+            assertThat(otherGift.mCollectCard).isEqualTo(this.mCollectCard);
             checkCollectByteArr(otherGift.mCollectByteArr, this.mCollectByteArr);
 
             assertThat(otherGift.mString).isEqualTo(this.mString);
@@ -141,7 +164,7 @@
             assertThat(otherGift.mBoxBoolean).isEqualTo(this.mBoxBoolean);
             assertThat(otherGift.mUnboxBoolean).isEqualTo(this.mUnboxBoolean);
             assertThat(otherGift.mUnboxByteArr).isEqualTo(this.mUnboxByteArr);
-            assertThat(otherGift.mGift).isEqualTo(this.mGift);
+            assertThat(otherGift.mCard).isEqualTo(this.mCard);
             return true;
         }
 
@@ -160,7 +183,7 @@
         //TODO(b/156296904) add test for int, float, GenericDocument, and class with
         // @AppSearchDocument annotation
         checkIsResultSuccess(mSession.setSchema(
-                new SetSchemaRequest.Builder().addDataClass(Gift.class).build()));
+                new SetSchemaRequest.Builder().addDataClass(Card.class, Gift.class).build()));
 
         // Create a Gift object and assign values.
         Gift inputDataClass = new Gift();
@@ -180,11 +203,11 @@
         inputDataClass.mArrUnboxInt = new int[]{5, 4};
         inputDataClass.mArrUnboxLong = new long[]{7, 6};
 
-        Gift innerGift1 = new Gift();
-        innerGift1.mUri = "innerGift.uri1";
-        Gift innerGift2 = new Gift();
-        innerGift2.mUri = "innerGift.uri2";
-        inputDataClass.mArrGift = new Gift[]{innerGift1, innerGift2};
+        Card card1 = new Card();
+        card1.mUri = "card.uri1";
+        Card card2 = new Card();
+        card2.mUri = "card.uri2";
+        inputDataClass.mArrCard = new Card[]{card2, card2};
 
         inputDataClass.mCollectLong = Arrays.asList(inputDataClass.mArrBoxLong);
         inputDataClass.mCollectInteger = Arrays.asList(inputDataClass.mArrBoxInteger);
@@ -193,7 +216,7 @@
         inputDataClass.mCollectDouble = Arrays.asList(inputDataClass.mArrBoxDouble);
         inputDataClass.mCollectFloat = Arrays.asList(inputDataClass.mArrBoxFloat);
         inputDataClass.mCollectByteArr = Arrays.asList(inputDataClass.mArrUnboxByteArr);
-        inputDataClass.mCollectGift = Arrays.asList(innerGift1, innerGift2);
+        inputDataClass.mCollectCard = Arrays.asList(card2, card2);
 
         inputDataClass.mString = "String";
         inputDataClass.mBoxLong = 1L;
@@ -207,7 +230,7 @@
         inputDataClass.mBoxBoolean = true;
         inputDataClass.mUnboxBoolean = false;
         inputDataClass.mUnboxByteArr = new byte[]{1, 2, 3};
-        inputDataClass.mGift = innerGift1;
+        inputDataClass.mCard = card1;
 
         // Index the Gift document and query it.
         checkIsBatchResultSuccess(mSession.putDocuments(
@@ -231,7 +254,7 @@
     public void testAnnotationProcessor_QueryByType() throws Exception {
         checkIsResultSuccess(mSession.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addDataClass(Gift.class)
+                        .addDataClass(Card.class, Gift.class)
                         .addSchema(AppSearchEmail.SCHEMA).build()));
 
         // Create documents and index them
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionTest.java
index 14f4e33..48129c8 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionTest.java
@@ -152,8 +152,8 @@
                 .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
                         .setDataType(PropertyConfig.DATA_TYPE_INT64)
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_NONE)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_NONE)
                         .build())
                 .build();
         checkIsResultSuccess(
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java
index ecb2b9e..1f1b399 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java
@@ -191,6 +191,7 @@
          * <p>If not specified, defaults to {@link
          * AppSearchSchema.PropertyConfig#INDEXING_TYPE_NONE} (the field will not be indexed and
          * cannot be queried).
+         * TODO(b/171857731) renamed to TermMatchType when using String-specific indexing config.
          */
         @AppSearchSchema.PropertyConfig.IndexingType int indexingType()
                 default AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
index 0af8dc1..eca9c5a 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
@@ -188,6 +188,10 @@
 
         // Find tokenizer type
         int tokenizerType = Integer.parseInt(params.get("tokenizerType").toString());
+        if (Integer.parseInt(params.get("indexingType").toString()) == 0) {
+            //TODO(b/171857731) remove this hack after apply to Icing lib's change.
+            tokenizerType = 0;
+        }
         ClassName tokenizerEnum;
         if (tokenizerType == 0 || isPropertyDocument) {  // TOKENIZER_TYPE_NONE
             //It is only valid for tokenizer_type to be 'NONE' if the data type is
diff --git a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
index 86f579d..94a5317 100644
--- a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
+++ b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
@@ -454,7 +454,8 @@
                         + "@AppSearchDocument\n"
                         + "public class Gift {\n"
                         + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(tokenizerType=100) String str;\n"
+                        + "  @AppSearchDocument.Property(indexingType=1, tokenizerType=100)\n"
+                        + "  String str;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining("Unknown tokenizer type 100");
     }
@@ -485,7 +486,8 @@
                         + "@AppSearchDocument\n"
                         + "public class Gift {\n"
                         + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(indexingType=100) String str;\n"
+                        + "  @AppSearchDocument.Property(indexingType=100, tokenizerType=1)\n"
+                        + "  String str;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining("Unknown indexing type 100");
     }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
index 4dfb30e..b256fa5 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
@@ -26,43 +26,43 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("stringProp")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("integerProp")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("longProp")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("floatProp")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("doubleProp")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("booleanProp")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("bytesProp")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_Field.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_Field.JAVA
index f9bea13..e315345 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_Field.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_Field.JAVA
@@ -21,7 +21,7 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_Getter.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_Getter.JAVA
index c30fa56..1cf9253 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_Getter.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_Getter.JAVA
@@ -21,7 +21,7 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
index 50f7553..13f4f18 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
@@ -24,25 +24,25 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("repeatReq")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("repeatNoReq")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("req")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("noReq")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
index 7855d50..87283d4 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
@@ -21,7 +21,7 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("indexNone")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("indexExact")
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
index 8a91767..8900092 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
@@ -21,7 +21,7 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrString")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
index 45459ae..d843153 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
@@ -21,7 +21,7 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("newName")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
index 8a6e8f6b..890d43d 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
@@ -21,7 +21,7 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
index 9f0d759..be28a52 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
@@ -26,25 +26,25 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("listOfString")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("setOfInt")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("repeatedByteArray")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("byteArray")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
index e4d80c9..8499df6 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
@@ -21,19 +21,19 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("cat")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("dog")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_AllSupportedTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_AllSupportedTypes.JAVA
index 9692936..d3ee29b 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_AllSupportedTypes.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_AllSupportedTypes.JAVA
@@ -32,43 +32,43 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectLong")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectInteger")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectDouble")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectFloat")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectBoolean")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectByteArr")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectString")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectGift")
@@ -81,79 +81,79 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxLong")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxLong")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxInteger")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxInt")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxDouble")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxDouble")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxFloat")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxFloat")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxBoolean")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxBoolean")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxByteArr")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxByteArr")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrString")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrGift")
@@ -166,73 +166,73 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("string")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxLong")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxLong")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxInteger")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxInt")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxDouble")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxDouble")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxFloat")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxFloat")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxBoolean")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxBoolean")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxByteArr")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("gift")
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
index 71ec562..9be6cdb 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
@@ -27,7 +27,7 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("tokPlain")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_MultipleSetters.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_MultipleSetters.JAVA
index 8a6e8f6b..890d43d 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_MultipleSetters.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_MultipleSetters.JAVA
@@ -21,7 +21,7 @@
           .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
             .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
diff --git a/benchmark/benchmark-macro-runtime/build.gradle b/benchmark/benchmark-macro-runtime/build.gradle
index 1bd0b20..ee7d933 100644
--- a/benchmark/benchmark-macro-runtime/build.gradle
+++ b/benchmark/benchmark-macro-runtime/build.gradle
@@ -37,7 +37,6 @@
 dependencies {
     api(JUNIT)
     api(KOTLIN_STDLIB)
-    api(KOTLIN_COROUTINES_ANDROID)
     api("androidx.annotation:annotation:1.1.0")
     implementation(project(":benchmark:benchmark-common"))
     implementation(project(":benchmark:benchmark-perfetto"))
@@ -78,3 +77,11 @@
     inceptionYear = "2020"
     description = "Android Benchmark - Macrobenchmark Runtime"
 }
+
+// Define a task dependency so the app is installed before we run macro benchmarks.
+tasks.getByPath(':benchmark:benchmark-macro-runtime:connectedCheck')
+        .dependsOn(
+                tasks.getByPath(
+                        ':benchmark:integration-tests:benchmark-simple-macro-benchmark-target:installRelease'
+                )
+        )
diff --git a/benchmark/benchmark-macro-runtime/src/androidTest/AndroidManifest.xml b/benchmark/benchmark-macro-runtime/src/androidTest/AndroidManifest.xml
index 11743e7..03b4e49 100644
--- a/benchmark/benchmark-macro-runtime/src/androidTest/AndroidManifest.xml
+++ b/benchmark/benchmark-macro-runtime/src/androidTest/AndroidManifest.xml
@@ -16,5 +16,15 @@
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="androidx.benchmark.macro.runtime.test">
+     <!--
+       The Macro Benchmark Sample needs to launch activities in
+       `androidx.benchmark.integration.macro.target` APK.
 
+        The Macro Benchmark Library uses `PackageManager` to query for activities. This requires
+        the test APK to declare that `androidx.benchmark.integration.macro.target` be visible to
+        the APK (given Android 11's package visibility rules).
+     -->
+    <queries>
+        <package android:name="androidx.benchmark.integration.macro.target" />
+    </queries>
 </manifest>
diff --git a/benchmark/benchmark-macro-runtime/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt b/benchmark/benchmark-macro-runtime/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt
new file mode 100644
index 0000000..f58c8ca
--- /dev/null
+++ b/benchmark/benchmark-macro-runtime/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.benchmark.macro.test
+
+import android.content.Intent
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.MacrobenchmarkScope
+import androidx.benchmark.macro.compile
+import androidx.benchmark.macro.device
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import junit.framework.TestCase.assertTrue
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.fail
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class ActionsTest {
+    @Test
+    @Ignore("Figure out why we can't launch the default activity.")
+    fun killTest() {
+        val scope = MacrobenchmarkScope(PACKAGE_NAME)
+        scope.pressHome()
+        scope.launchPackageAndWait { intent ->
+            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+        }
+        assertTrue(isProcessAlive(PACKAGE_NAME))
+        scope.killProcess()
+        assertFalse(isProcessAlive(PACKAGE_NAME))
+    }
+
+    @Test
+    @Ignore("Compilation modes are a bit flaky")
+    fun compile_speedProfile() {
+        val scope = MacrobenchmarkScope(PACKAGE_NAME)
+        val iterations = 1
+        var executions = 0
+        val compilation = CompilationMode.SpeedProfile(warmupIterations = iterations)
+        compilation.compile(PACKAGE_NAME) {
+            executions += 1
+            scope.pressHome()
+            scope.launchPackageAndWait { intent ->
+                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+            }
+        }
+        assertEquals(iterations, executions)
+    }
+
+    @Test
+    @Ignore("Compilation modes are a bit flaky")
+    fun compile_speed() {
+        val compilation = CompilationMode.Speed
+        compilation.compile(PACKAGE_NAME) {
+            fail("Should never be called for $compilation")
+        }
+    }
+
+    private fun processes(): List<String> {
+        val instrumentation = InstrumentationRegistry.getInstrumentation()
+        val output = instrumentation.device().executeShellCommand("ps -A")
+        return output.split("\r?\n".toRegex())
+    }
+
+    private fun isProcessAlive(packageName: String): Boolean {
+        return processes().any { it.contains(packageName) }
+    }
+
+    companion object {
+        private const val PACKAGE_NAME = "androidx.benchmark.integration.macro.target"
+    }
+}
diff --git a/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/Actions.kt b/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/Actions.kt
index 89174e7..544a39e 100644
--- a/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/Actions.kt
+++ b/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/Actions.kt
@@ -20,8 +20,6 @@
 import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.UiDevice
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.runBlocking
 
 private const val TAG = "MacroBenchmarks"
 
@@ -57,13 +55,14 @@
 
 /**
  * Compiles the application with the specified filter.
+ * For more information: https://source.android.com/devices/tech/dalvik/jit-compiler
  */
 internal fun compilationFilter(
     instrumentation: Instrumentation,
     packageName: String,
     mode: String,
     profileSaveTimeout: Long = 5000
-) = runBlocking {
+) {
     check(mode in COMPILE_MODES) {
         "Invalid compilation mode. Must be one of ${COMPILE_MODES.joinToString(",")}"
     }
@@ -72,14 +71,13 @@
         // For speed profile compilation, ART team recommended to wait for 5 secs when app
         // is in the foreground, dump the profile, wait for another 5 secs before
         // speed-profile compilation.
-        delay(profileSaveTimeout)
+        Thread.sleep(profileSaveTimeout)
         val response = device.executeShellCommand("killall -s SIGUSR1 $packageName")
         if (response.isNotBlank()) {
             Log.d(TAG, "Received dump profile response $response")
-        } else {
             throw RuntimeException("Failed to dump profile for $packageName ($response)")
         }
-        delay(profileSaveTimeout)
+        Thread.sleep(profileSaveTimeout)
     }
     val response = device.executeShellCommand("cmd package compile -f -m $mode $packageName")
     if (!response.contains("Success")) {
@@ -89,12 +87,22 @@
 }
 
 /**
+ * Clears existing compilation profiles.
+ */
+internal fun clearProfile(
+    instrumentation: Instrumentation,
+    packageName: String,
+) {
+    instrumentation.device().executeShellCommand("cmd package compile --reset $packageName")
+}
+
+/**
  * Presses the home button.
  */
-fun pressHome(instrumentation: Instrumentation, delayDurationMs: Long = 300) = runBlocking {
+fun pressHome(instrumentation: Instrumentation, delayDurationMs: Long = 300) {
     instrumentation.device().pressHome()
     // Sleep for statsd to update the metrics.
-    delay(delayDurationMs)
+    Thread.sleep(delayDurationMs)
 }
 
 /**
@@ -135,6 +143,6 @@
     instrumentation.device().executeShellCommand("setenforce $policy")
 }
 
-private fun Instrumentation.device(): UiDevice {
+internal fun Instrumentation.device(): UiDevice {
     return UiDevice.getInstance(this)
 }
diff --git a/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 586f917..bb93fa8 100644
--- a/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -26,7 +26,16 @@
         }
         return compileArgument
     }
-    object None : CompilationMode(null)
-    class SpeedProfile(val warmupIterations: Int = 3) : CompilationMode("speed-profile")
-    object Speed : CompilationMode("speed")
-}
\ No newline at end of file
+
+    object None : CompilationMode(null) {
+        override fun toString() = "CompilationMode.None"
+    }
+
+    class SpeedProfile(val warmupIterations: Int = 3) : CompilationMode("speed-profile") {
+        override fun toString() = "CompilationMode.SpeedProfile (iterations =$warmupIterations)"
+    }
+
+    object Speed : CompilationMode("speed") {
+        override fun toString() = "CompilationMode.Speed"
+    }
+}
diff --git a/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index d07d5b9..12feb34 100644
--- a/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -26,7 +26,7 @@
  * Provides access to common operations in app automation, such as killing the app,
  * or navigating home.
  */
-class MacrobenchmarkScope(
+public class MacrobenchmarkScope(
     private val packageName: String
 ) {
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
@@ -53,6 +53,14 @@
     }
 }
 
+data class MacrobenchmarkConfig(
+    val packageName: String,
+    val metrics: List<Metric>,
+    val compilationMode: CompilationMode = CompilationMode.SpeedProfile(),
+    val killProcessEachIteration: Boolean = false,
+    val iterations: Int
+)
+
 /**
  * Primary macrobenchmark test entrypoint.
  *
@@ -60,54 +68,52 @@
  */
 fun macrobenchmark(
     benchmarkName: String,
-    packageName: String,
-    metrics: List<Metric>,
-    compilationMode: CompilationMode = CompilationMode.SpeedProfile(),
-    killProcessEachIteration: Boolean = false,
-    iterations: Int,
+    config: MacrobenchmarkConfig,
     block: MacrobenchmarkScope.() -> Unit
 ) = withPermissiveSeLinuxPolicy {
-    val scope = MacrobenchmarkScope(packageName)
+    val scope = MacrobenchmarkScope(config.packageName)
 
     // always kill the process at beginning of test
     scope.killProcess()
 
-    compilationMode.compile(packageName) {
+    config.compilationMode.compile(config.packageName) {
         block(scope)
     }
 
     // Perfetto collector is separate from metrics, so we can control file
     // output, and give it different (test-wide) lifecycle
-    val perfettoCollector = PerfettoCollector("$benchmarkName.trace")
+    val perfettoCollector = PerfettoCaptureWrapper()
     try {
         perfettoCollector.start()
-        metrics.forEach {
+        config.metrics.forEach {
             it.start()
         }
-        repeat(iterations) {
-            if (killProcessEachIteration) {
+        repeat(config.iterations) {
+            if (config.killProcessEachIteration) {
                 scope.killProcess()
             }
             block(scope)
         }
-        metrics.forEach {
+        config.metrics.forEach {
             it.stop()
         }
-        metrics.map {
+        config.metrics.map {
             it.collector
         }.report()
     } finally {
-        perfettoCollector.stop()
+        perfettoCollector.stop("$benchmarkName.trace")
         scope.killProcess()
     }
 }
 
-private fun CompilationMode.compile(packageName: String, block: () -> Unit) {
+internal fun CompilationMode.compile(packageName: String, block: () -> Unit) {
+    val instrumentation = InstrumentationRegistry.getInstrumentation()
+    // Clear profile between runs.
+    clearProfile(instrumentation, packageName)
     if (this == CompilationMode.None) {
         return // nothing to do
     }
     if (this is CompilationMode.SpeedProfile) {
-        // TODO: clear existing profiling state
         repeat(this.warmupIterations) {
             block()
         }
diff --git a/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt b/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
new file mode 100644
index 0000000..babaad7
--- /dev/null
+++ b/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.benchmark.macro
+
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * JUnit rule for benchmarking large app operations like startup.
+ */
+class MacrobenchmarkRule : TestRule {
+    lateinit var benchmarkName: String
+
+    fun measureRepeated(
+        config: MacrobenchmarkConfig,
+        block: MacrobenchmarkScope.() -> Unit
+    ) {
+        macrobenchmark(benchmarkName, config, block)
+    }
+
+    override fun apply(base: Statement, description: Description) = object : Statement() {
+        override fun evaluate() {
+            benchmarkName = description.toUniqueName()
+            base.evaluate()
+        }
+    }
+
+    internal fun Description.toUniqueName() = testClass.simpleName + "_" + methodName
+}
diff --git a/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/PerfettoCollector.kt b/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
similarity index 82%
rename from benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/PerfettoCollector.kt
rename to benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
index 2b0cbef..88014df 100644
--- a/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/PerfettoCollector.kt
+++ b/benchmark/benchmark-macro-runtime/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
@@ -23,9 +23,9 @@
 import androidx.benchmark.perfetto.reportAdditionalFileToCopy
 
 /**
- * Helps capture Perfetto traces.
+ * Wrapper for PerfettoCapture, which does nothing on API < Q
  */
-class PerfettoCollector(private val traceName: String) : Collector<Unit> {
+class PerfettoCaptureWrapper {
     private var capture: PerfettoCapture? = null
 
     init {
@@ -34,15 +34,15 @@
         }
     }
 
-    override fun start(): Boolean {
+    fun start(): Boolean {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-            Log.d(TAG, "Recording perfetto trace $traceName")
+            Log.d(TAG, "Recording perfetto trace")
             capture?.start()
         }
         return true
     }
 
-    override fun stop(): Boolean {
+    fun stop(traceName: String): Boolean {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
             val destination = destinationPath(traceName).absolutePath
             capture?.stop(destination)
@@ -51,10 +51,6 @@
         return true
     }
 
-    override fun metrics(): Map<String, Unit> {
-        return emptyMap()
-    }
-
     companion object {
         private const val TAG = "PerfettoCollector"
     }
diff --git a/benchmark/benchmark-simple-macro-benchmark/src/androidTest/java/androidx/benchmark/macro/sample/MacroBenchmarkTest.kt b/benchmark/benchmark-simple-macro-benchmark/src/androidTest/java/androidx/benchmark/macro/sample/MacroBenchmarkTest.kt
index 4a2f637..a7359f6 100644
--- a/benchmark/benchmark-simple-macro-benchmark/src/androidTest/java/androidx/benchmark/macro/sample/MacroBenchmarkTest.kt
+++ b/benchmark/benchmark-simple-macro-benchmark/src/androidTest/java/androidx/benchmark/macro/sample/MacroBenchmarkTest.kt
@@ -19,27 +19,32 @@
 import android.content.Intent
 import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.CpuUsageMetric
+import androidx.benchmark.macro.MacrobenchmarkConfig
+import androidx.benchmark.macro.MacrobenchmarkRule
 import androidx.benchmark.macro.StartupTimingMetric
-import androidx.benchmark.macro.macrobenchmark
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import org.junit.Ignore
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class MacroBenchmarkTest {
+    @get:Rule
+    val benchmarkRule = MacrobenchmarkRule()
+
     @Test
-    @LargeTest
     @Ignore("Not running the test in CI")
-    fun basicTest() = macrobenchmark(
-        "benchmarkUniqueName",
-        packageName = "androidx.benchmark.integration.macro.target",
-        listOf(StartupTimingMetric(), CpuUsageMetric()),
-        CompilationMode.Speed,
-        killProcessEachIteration = true,
-        iterations = 4
+    fun basicTest() = benchmarkRule.measureRepeated(
+        MacrobenchmarkConfig(
+            packageName = "androidx.benchmark.integration.macro.target",
+            listOf(StartupTimingMetric(), CpuUsageMetric()),
+            CompilationMode.Speed,
+            killProcessEachIteration = true,
+            iterations = 4
+        )
     ) {
         pressHome()
         launchPackageAndWait { launchIntent ->
diff --git a/benchmark/benchmark-simple-macro-benchmark/src/androidTest/java/androidx/benchmark/macro/sample/ProcessSpeedProfileValidation.kt b/benchmark/benchmark-simple-macro-benchmark/src/androidTest/java/androidx/benchmark/macro/sample/ProcessSpeedProfileValidation.kt
new file mode 100644
index 0000000..7cef109
--- /dev/null
+++ b/benchmark/benchmark-simple-macro-benchmark/src/androidTest/java/androidx/benchmark/macro/sample/ProcessSpeedProfileValidation.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.benchmark.macro.sample
+
+import android.content.Intent
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.CpuUsageMetric
+import androidx.benchmark.macro.MacrobenchmarkConfig
+import androidx.benchmark.macro.StartupTimingMetric
+import androidx.benchmark.macro.macrobenchmark
+import androidx.test.filters.LargeTest
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class ProcessSpeedProfileValidation(
+    private val compilationMode: CompilationMode,
+    private val killProcess: Boolean
+) {
+    @Test
+    @Ignore("Not running the test in CI")
+    fun start() {
+        val benchmarkName = "speed_profile_process_validation"
+        val config = MacrobenchmarkConfig(
+            packageName = PACKAGE_NAME,
+            metrics = listOf(CpuUsageMetric(), StartupTimingMetric()),
+            compilationMode = compilationMode,
+            killProcessEachIteration = killProcess,
+            iterations = 10
+        )
+        macrobenchmark(
+            benchmarkName = benchmarkName,
+            config = config
+        ) {
+            pressHome()
+            launchPackageAndWait { launchIntent ->
+                // Clear out any previous instances
+                launchIntent.flags =
+                    Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+            }
+        }
+    }
+
+    companion object {
+        private const val PACKAGE_NAME = "androidx.benchmark.integration.macro.target"
+
+        @Parameterized.Parameters(name = "compilation_mode={0}, kill_process={1}")
+        @JvmStatic
+        fun kilProcessParameters(): List<Array<Any>> {
+            val compilationModes = listOf(
+                CompilationMode.None,
+                CompilationMode.SpeedProfile(warmupIterations = 3)
+            )
+            val processKillOptions = listOf(true, false)
+            return compilationModes.zip(processKillOptions).map {
+                arrayOf(it.first, it.second)
+            }
+        }
+    }
+}
diff --git a/benchmark/benchmark/build.gradle b/benchmark/benchmark/build.gradle
index b0d1aa1..98a9387 100644
--- a/benchmark/benchmark/build.gradle
+++ b/benchmark/benchmark/build.gradle
@@ -22,8 +22,17 @@
     id("androidx.benchmark")
 }
 
+android {
+    defaultConfig {
+        // 18 needed for UI automator dependency, via benchmark-perfetto
+        minSdkVersion 18
+    }
+}
+
 dependencies {
     androidTestImplementation(project(":benchmark:benchmark-junit4"))
+    androidTestImplementation(project(":benchmark:benchmark-perfetto"))
+    androidTestImplementation(project(":tracing:tracing-ktx"))
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(ANDROIDX_TEST_RULES)
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoOverheadBenchmark.kt b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoOverheadBenchmark.kt
new file mode 100644
index 0000000..f3f224c
--- /dev/null
+++ b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoOverheadBenchmark.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.benchmark.benchmark
+
+import android.os.Trace
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.benchmark.perfetto.PerfettoRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.tracing.trace
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class PerfettoOverheadBenchmark {
+    @get:Rule
+    val benchmarkRule = BenchmarkRule()
+
+    @get:Rule
+    val perfettoRule = PerfettoRule()
+
+    /**
+     * Empty baseline, no tracing. Expect similar results to [TrivialJavaBenchmark.nothing].
+     */
+    @Test
+    fun empty() = benchmarkRule.measureRepeated {}
+
+    /**
+     * The trace section within runWithTimingDisabled, even though not measured, can impact the
+     * results of a small benchmark significantly.
+     */
+    @Test
+    fun runWithTimingDisabled() = benchmarkRule.measureRepeated {
+        runWithTimingDisabled { /* nothing*/ }
+    }
+
+    /**
+     * Trace section adds ~5us (depending on many factors) in this ideal case, but will be
+     * significantly worse in a real benchmark, as there's more computation to interfere with.
+     */
+    @Test
+    fun traceBeginEnd() = benchmarkRule.measureRepeated {
+        Trace.beginSection("foo")
+        Trace.endSection()
+    }
+
+    /**
+     * Dupe of [traceBeginEnd], just using [trace].
+     */
+    @Test
+    fun traceBlock() = benchmarkRule.measureRepeated {
+        trace("foo") { /* nothing */ }
+    }
+}
diff --git a/benchmark/common/src/androidTest/java/androidx/benchmark/StatsTest.kt b/benchmark/common/src/androidTest/java/androidx/benchmark/StatsTest.kt
index 01455bb..4af0b67 100644
--- a/benchmark/common/src/androidTest/java/androidx/benchmark/StatsTest.kt
+++ b/benchmark/common/src/androidTest/java/androidx/benchmark/StatsTest.kt
@@ -34,8 +34,6 @@
         assertEquals(10, stats.max)
         assertEquals(10, stats.min)
         assertEquals(0.0, stats.standardDeviation, 0.0)
-        assertEquals(10, stats.percentile90)
-        assertEquals(10, stats.percentile95)
     }
 
     @Test
@@ -46,8 +44,6 @@
         assertEquals(10, stats.max)
         assertEquals(10, stats.min)
         assertEquals(Double.NaN, stats.standardDeviation, 0.0)
-        assertEquals(10, stats.percentile90)
-        assertEquals(10, stats.percentile95)
     }
 
     @Test
@@ -58,8 +54,6 @@
         assertEquals(100, stats.max)
         assertEquals(1, stats.min)
         assertEquals(29.01, stats.standardDeviation, 0.05)
-        assertEquals(90, stats.percentile90)
-        assertEquals(95, stats.percentile95)
     }
 
     @Test
@@ -76,11 +70,4 @@
             assertEquals(it.toLong(), Stats.getPercentile(listOf(0L, 25L, 50L, 75L, 100L), it))
         }
     }
-
-    @Test
-    fun fractionalPercentile() {
-        val stats = Stats(longArrayOf(0L, 25L, 50L, 75L, 100L), "test")
-        assertEquals(90, stats.percentile90)
-        assertEquals(95, stats.percentile95)
-    }
 }
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Stats.kt b/benchmark/common/src/main/java/androidx/benchmark/Stats.kt
index 368fbcf..7937c01 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/Stats.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/Stats.kt
@@ -29,8 +29,6 @@
     val median: Long
     val min: Long
     val max: Long
-    val percentile90: Long
-    val percentile95: Long
     val mean: Double = data.average()
     val standardDeviation: Double
 
@@ -44,8 +42,6 @@
         min = values.first()
         max = values.last()
         median = getPercentile(values, 50)
-        percentile90 = getPercentile(values, 90)
-        percentile95 = getPercentile(values, 95)
         standardDeviation = if (size == 1) {
             NaN
         } else {
@@ -82,7 +78,7 @@
 
     override fun hashCode(): Int {
         return min.hashCode() + max.hashCode() + median.hashCode() + standardDeviation.hashCode() +
-            mean.hashCode() + percentile90.hashCode() + percentile95.hashCode()
+            mean.hashCode()
     }
 
     companion object {
diff --git a/benchmark/macro/src/main/res/raw/trace_config.textproto b/benchmark/macro/src/main/res/raw/trace_config.textproto
deleted file mode 100644
index d2d3500..0000000
--- a/benchmark/macro/src/main/res/raw/trace_config.textproto
+++ /dev/null
@@ -1,119 +0,0 @@
-# Copyright (C) 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.
-
-# proto-message: TraceConfig
-
-# Enable periodic flushing of the trace buffer into the output file.
-write_into_file: true
-
-# Writes the userspace buffer into the file every 1s.
-file_write_period_ms: 1000
-
-# See b/126487238 - we need to guarantee ordering of events.
-flush_period_ms: 30000
-
-# The trace buffers needs to be big enough to hold |file_write_period_ms| of
-# trace data. The trace buffer sizing depends on the number of trace categories
-# enabled and the device activity.
-
-# RSS events
-buffers {
-  size_kb: 16384
-  fill_policy: RING_BUFFER
-}
-
-# procfs polling
-buffers {
-  size_kb: 8192
-  fill_policy: RING_BUFFER
-}
-
-data_sources {
-  config {
-    name: "linux.ftrace"
-    target_buffer: 0
-    ftrace_config {
-      # These parameters affect only the kernel trace buffer size and how
-      # frequently it gets moved into the userspace buffer defined above.
-      buffer_size_kb: 16384
-      drain_period_ms: 250
-
-      # We need to do process tracking to ensure kernel ftrace events targeted at short-lived
-      # threads are associated correctly
-      ftrace_events: "task/task_newtask"
-      ftrace_events: "task/task_rename"
-      ftrace_events: "sched/sched_process_exit"
-      ftrace_events: "sched/sched_process_free"
-
-      # Memory events
-      ftrace_events: "rss_stat"
-      ftrace_events: "ion_heap_shrink"
-      ftrace_events: "ion_heap_grow"
-      ftrace_events: "ion/ion_stat"
-      ftrace_events: "oom_score_adj_update"
-
-      # Old (kernel) LMK
-      ftrace_events: "lowmemorykiller/lowmemory_kill"
-
-      # New (userspace) LMK
-      atrace_apps: "lmkd"
-      # Added for userspace annotation in the platform scenario test app
-      atrace_apps: "android.platform.test.scenario"
-
-      atrace_categories: "am"
-      atrace_categories: "dalvik"
-      atrace_categories: "binder_driver"
-    }
-  }
-}
-
-data_sources {
-  config {
-    name: "linux.process_stats"
-    target_buffer: 1
-    process_stats_config {
-      proc_stats_poll_ms: 10000
-    }
-  }
-}
-
-data_sources {
-  config {
-    name: "linux.sys_stats"
-    target_buffer: 1
-    sys_stats_config {
-      meminfo_period_ms: 1000
-      meminfo_counters: MEMINFO_MEM_TOTAL
-      meminfo_counters: MEMINFO_MEM_FREE
-      meminfo_counters: MEMINFO_MEM_AVAILABLE
-      meminfo_counters: MEMINFO_BUFFERS
-      meminfo_counters: MEMINFO_CACHED
-      meminfo_counters: MEMINFO_SWAP_CACHED
-      meminfo_counters: MEMINFO_ACTIVE
-      meminfo_counters: MEMINFO_INACTIVE
-      meminfo_counters: MEMINFO_ACTIVE_ANON
-      meminfo_counters: MEMINFO_INACTIVE_ANON
-      meminfo_counters: MEMINFO_ACTIVE_FILE
-      meminfo_counters: MEMINFO_INACTIVE_FILE
-      meminfo_counters: MEMINFO_UNEVICTABLE
-      meminfo_counters: MEMINFO_SWAP_TOTAL
-      meminfo_counters: MEMINFO_SWAP_FREE
-      meminfo_counters: MEMINFO_DIRTY
-      meminfo_counters: MEMINFO_WRITEBACK
-      meminfo_counters: MEMINFO_ANON_PAGES
-      meminfo_counters: MEMINFO_MAPPED
-      meminfo_counters: MEMINFO_SHMEM
-    }
-  }
-}
\ No newline at end of file
diff --git a/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoRule.kt b/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoRule.kt
index eb69b8c..e562cb8 100644
--- a/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoRule.kt
+++ b/benchmark/perfetto/src/main/java/androidx/benchmark/perfetto/PerfettoRule.kt
@@ -91,6 +91,7 @@
         block()
         val dst = destinationPath(traceName)
         stop(dst.absolutePath)
+        Log.d(PerfettoRule.TAG, "Finished recording to ${dst.absolutePath}")
         reportAdditionalFileToCopy("perfetto_trace", dst.absolutePath)
     } finally {
         cancel()
diff --git a/biometric/biometric/api/1.2.0-alpha01.txt b/biometric/biometric/api/1.2.0-alpha01.txt
new file mode 100644
index 0000000..2d2401f
--- /dev/null
+++ b/biometric/biometric/api/1.2.0-alpha01.txt
@@ -0,0 +1,96 @@
+// Signature format: 4.0
+package androidx.biometric {
+
+  public class BiometricManager {
+    method @Deprecated public int canAuthenticate();
+    method public int canAuthenticate(int);
+    method public static androidx.biometric.BiometricManager from(android.content.Context);
+    field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field public static final int BIOMETRIC_ERROR_NONE_ENROLLED = 11; // 0xb
+    field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc
+    field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+    field public static final int BIOMETRIC_ERROR_UNSUPPORTED = -2; // 0xfffffffe
+    field public static final int BIOMETRIC_STATUS_UNKNOWN = -1; // 0xffffffff
+    field public static final int BIOMETRIC_SUCCESS = 0; // 0x0
+  }
+
+  public static interface BiometricManager.Authenticators {
+    field public static final int BIOMETRIC_STRONG = 15; // 0xf
+    field public static final int BIOMETRIC_WEAK = 255; // 0xff
+    field public static final int DEVICE_CREDENTIAL = 32768; // 0x8000
+  }
+
+  public class BiometricPrompt {
+    ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.Fragment, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo, androidx.biometric.BiometricPrompt.CryptoObject);
+    method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo);
+    method public void cancelAuthentication();
+    field public static final int AUTHENTICATION_RESULT_TYPE_BIOMETRIC = 2; // 0x2
+    field public static final int AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL = 1; // 0x1
+    field public static final int AUTHENTICATION_RESULT_TYPE_UNKNOWN = -1; // 0xffffffff
+    field public static final int ERROR_CANCELED = 5; // 0x5
+    field public static final int ERROR_HW_NOT_PRESENT = 12; // 0xc
+    field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field public static final int ERROR_LOCKOUT = 7; // 0x7
+    field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+    field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
+    field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
+    field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
+    field public static final int ERROR_NO_SPACE = 4; // 0x4
+    field public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+    field public static final int ERROR_TIMEOUT = 3; // 0x3
+    field public static final int ERROR_UNABLE_TO_PROCESS = 2; // 0x2
+    field public static final int ERROR_USER_CANCELED = 10; // 0xa
+    field public static final int ERROR_VENDOR = 8; // 0x8
+  }
+
+  public abstract static class BiometricPrompt.AuthenticationCallback {
+    ctor public BiometricPrompt.AuthenticationCallback();
+    method public void onAuthenticationError(int, CharSequence);
+    method public void onAuthenticationFailed();
+    method public void onAuthenticationSucceeded(androidx.biometric.BiometricPrompt.AuthenticationResult);
+  }
+
+  public static class BiometricPrompt.AuthenticationResult {
+    method public int getAuthenticationType();
+    method public androidx.biometric.BiometricPrompt.CryptoObject? getCryptoObject();
+  }
+
+  public static class BiometricPrompt.CryptoObject {
+    ctor public BiometricPrompt.CryptoObject(java.security.Signature);
+    ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
+    ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public BiometricPrompt.CryptoObject(android.security.identity.IdentityCredential);
+    method public javax.crypto.Cipher? getCipher();
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
+    method public javax.crypto.Mac? getMac();
+    method public java.security.Signature? getSignature();
+  }
+
+  public static class BiometricPrompt.PromptInfo {
+    method public int getAllowedAuthenticators();
+    method public CharSequence? getDescription();
+    method public CharSequence getNegativeButtonText();
+    method public CharSequence? getSubtitle();
+    method public CharSequence getTitle();
+    method public boolean isConfirmationRequired();
+    method @Deprecated public boolean isDeviceCredentialAllowed();
+  }
+
+  public static class BiometricPrompt.PromptInfo.Builder {
+    ctor public BiometricPrompt.PromptInfo.Builder();
+    method public androidx.biometric.BiometricPrompt.PromptInfo build();
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
+    method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
+  }
+
+}
+
diff --git a/biometric/biometric/api/public_plus_experimental_1.2.0-alpha01.txt b/biometric/biometric/api/public_plus_experimental_1.2.0-alpha01.txt
new file mode 100644
index 0000000..2d2401f
--- /dev/null
+++ b/biometric/biometric/api/public_plus_experimental_1.2.0-alpha01.txt
@@ -0,0 +1,96 @@
+// Signature format: 4.0
+package androidx.biometric {
+
+  public class BiometricManager {
+    method @Deprecated public int canAuthenticate();
+    method public int canAuthenticate(int);
+    method public static androidx.biometric.BiometricManager from(android.content.Context);
+    field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field public static final int BIOMETRIC_ERROR_NONE_ENROLLED = 11; // 0xb
+    field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc
+    field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+    field public static final int BIOMETRIC_ERROR_UNSUPPORTED = -2; // 0xfffffffe
+    field public static final int BIOMETRIC_STATUS_UNKNOWN = -1; // 0xffffffff
+    field public static final int BIOMETRIC_SUCCESS = 0; // 0x0
+  }
+
+  public static interface BiometricManager.Authenticators {
+    field public static final int BIOMETRIC_STRONG = 15; // 0xf
+    field public static final int BIOMETRIC_WEAK = 255; // 0xff
+    field public static final int DEVICE_CREDENTIAL = 32768; // 0x8000
+  }
+
+  public class BiometricPrompt {
+    ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.Fragment, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo, androidx.biometric.BiometricPrompt.CryptoObject);
+    method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo);
+    method public void cancelAuthentication();
+    field public static final int AUTHENTICATION_RESULT_TYPE_BIOMETRIC = 2; // 0x2
+    field public static final int AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL = 1; // 0x1
+    field public static final int AUTHENTICATION_RESULT_TYPE_UNKNOWN = -1; // 0xffffffff
+    field public static final int ERROR_CANCELED = 5; // 0x5
+    field public static final int ERROR_HW_NOT_PRESENT = 12; // 0xc
+    field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field public static final int ERROR_LOCKOUT = 7; // 0x7
+    field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+    field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
+    field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
+    field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
+    field public static final int ERROR_NO_SPACE = 4; // 0x4
+    field public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+    field public static final int ERROR_TIMEOUT = 3; // 0x3
+    field public static final int ERROR_UNABLE_TO_PROCESS = 2; // 0x2
+    field public static final int ERROR_USER_CANCELED = 10; // 0xa
+    field public static final int ERROR_VENDOR = 8; // 0x8
+  }
+
+  public abstract static class BiometricPrompt.AuthenticationCallback {
+    ctor public BiometricPrompt.AuthenticationCallback();
+    method public void onAuthenticationError(int, CharSequence);
+    method public void onAuthenticationFailed();
+    method public void onAuthenticationSucceeded(androidx.biometric.BiometricPrompt.AuthenticationResult);
+  }
+
+  public static class BiometricPrompt.AuthenticationResult {
+    method public int getAuthenticationType();
+    method public androidx.biometric.BiometricPrompt.CryptoObject? getCryptoObject();
+  }
+
+  public static class BiometricPrompt.CryptoObject {
+    ctor public BiometricPrompt.CryptoObject(java.security.Signature);
+    ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
+    ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public BiometricPrompt.CryptoObject(android.security.identity.IdentityCredential);
+    method public javax.crypto.Cipher? getCipher();
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
+    method public javax.crypto.Mac? getMac();
+    method public java.security.Signature? getSignature();
+  }
+
+  public static class BiometricPrompt.PromptInfo {
+    method public int getAllowedAuthenticators();
+    method public CharSequence? getDescription();
+    method public CharSequence getNegativeButtonText();
+    method public CharSequence? getSubtitle();
+    method public CharSequence getTitle();
+    method public boolean isConfirmationRequired();
+    method @Deprecated public boolean isDeviceCredentialAllowed();
+  }
+
+  public static class BiometricPrompt.PromptInfo.Builder {
+    ctor public BiometricPrompt.PromptInfo.Builder();
+    method public androidx.biometric.BiometricPrompt.PromptInfo build();
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
+    method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
+  }
+
+}
+
diff --git a/biometric/biometric/api/res-1.2.0-alpha01.txt b/biometric/biometric/api/res-1.2.0-alpha01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/biometric/biometric/api/res-1.2.0-alpha01.txt
diff --git a/biometric/biometric/api/restricted_1.2.0-alpha01.txt b/biometric/biometric/api/restricted_1.2.0-alpha01.txt
new file mode 100644
index 0000000..2d2401f
--- /dev/null
+++ b/biometric/biometric/api/restricted_1.2.0-alpha01.txt
@@ -0,0 +1,96 @@
+// Signature format: 4.0
+package androidx.biometric {
+
+  public class BiometricManager {
+    method @Deprecated public int canAuthenticate();
+    method public int canAuthenticate(int);
+    method public static androidx.biometric.BiometricManager from(android.content.Context);
+    field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field public static final int BIOMETRIC_ERROR_NONE_ENROLLED = 11; // 0xb
+    field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc
+    field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+    field public static final int BIOMETRIC_ERROR_UNSUPPORTED = -2; // 0xfffffffe
+    field public static final int BIOMETRIC_STATUS_UNKNOWN = -1; // 0xffffffff
+    field public static final int BIOMETRIC_SUCCESS = 0; // 0x0
+  }
+
+  public static interface BiometricManager.Authenticators {
+    field public static final int BIOMETRIC_STRONG = 15; // 0xf
+    field public static final int BIOMETRIC_WEAK = 255; // 0xff
+    field public static final int DEVICE_CREDENTIAL = 32768; // 0x8000
+  }
+
+  public class BiometricPrompt {
+    ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.Fragment, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo, androidx.biometric.BiometricPrompt.CryptoObject);
+    method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo);
+    method public void cancelAuthentication();
+    field public static final int AUTHENTICATION_RESULT_TYPE_BIOMETRIC = 2; // 0x2
+    field public static final int AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL = 1; // 0x1
+    field public static final int AUTHENTICATION_RESULT_TYPE_UNKNOWN = -1; // 0xffffffff
+    field public static final int ERROR_CANCELED = 5; // 0x5
+    field public static final int ERROR_HW_NOT_PRESENT = 12; // 0xc
+    field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field public static final int ERROR_LOCKOUT = 7; // 0x7
+    field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+    field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
+    field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
+    field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
+    field public static final int ERROR_NO_SPACE = 4; // 0x4
+    field public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+    field public static final int ERROR_TIMEOUT = 3; // 0x3
+    field public static final int ERROR_UNABLE_TO_PROCESS = 2; // 0x2
+    field public static final int ERROR_USER_CANCELED = 10; // 0xa
+    field public static final int ERROR_VENDOR = 8; // 0x8
+  }
+
+  public abstract static class BiometricPrompt.AuthenticationCallback {
+    ctor public BiometricPrompt.AuthenticationCallback();
+    method public void onAuthenticationError(int, CharSequence);
+    method public void onAuthenticationFailed();
+    method public void onAuthenticationSucceeded(androidx.biometric.BiometricPrompt.AuthenticationResult);
+  }
+
+  public static class BiometricPrompt.AuthenticationResult {
+    method public int getAuthenticationType();
+    method public androidx.biometric.BiometricPrompt.CryptoObject? getCryptoObject();
+  }
+
+  public static class BiometricPrompt.CryptoObject {
+    ctor public BiometricPrompt.CryptoObject(java.security.Signature);
+    ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
+    ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public BiometricPrompt.CryptoObject(android.security.identity.IdentityCredential);
+    method public javax.crypto.Cipher? getCipher();
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
+    method public javax.crypto.Mac? getMac();
+    method public java.security.Signature? getSignature();
+  }
+
+  public static class BiometricPrompt.PromptInfo {
+    method public int getAllowedAuthenticators();
+    method public CharSequence? getDescription();
+    method public CharSequence getNegativeButtonText();
+    method public CharSequence? getSubtitle();
+    method public CharSequence getTitle();
+    method public boolean isConfirmationRequired();
+    method @Deprecated public boolean isDeviceCredentialAllowed();
+  }
+
+  public static class BiometricPrompt.PromptInfo.Builder {
+    ctor public BiometricPrompt.PromptInfo.Builder();
+    method public androidx.biometric.BiometricPrompt.PromptInfo build();
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
+    method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
+  }
+
+}
+
diff --git a/browser/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPoolTest.java b/browser/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPoolTest.java
index eb2f23a..a7fc625 100644
--- a/browser/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPoolTest.java
+++ b/browser/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPoolTest.java
@@ -36,7 +36,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -45,7 +44,6 @@
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
 
 @RunWith(AndroidJUnit4.class)
 @MediumTest
@@ -118,17 +116,4 @@
             fail();
         }
     }
-
-    @Ignore("Test disabled due to flakiness, see b/153851530")
-    @Test
-    public void testMultipleExecutions() {
-        final AtomicInteger count = new AtomicInteger();
-
-        mManager.connect(GOOD_SCOPE, mTrustedPackages, android.os.AsyncTask.THREAD_POOL_EXECUTOR)
-                .addListener(count::incrementAndGet, android.os.AsyncTask.THREAD_POOL_EXECUTOR);
-        mManager.connect(GOOD_SCOPE, mTrustedPackages, android.os.AsyncTask.THREAD_POOL_EXECUTOR)
-                .addListener(count::incrementAndGet, android.os.AsyncTask.THREAD_POOL_EXECUTOR);
-
-        PollingCheck.waitFor(() -> count.get() == 2);
-    }
 }
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 2bb6f18..d94be3c 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -32,7 +32,7 @@
     val ASYNCLAYOUTINFLATER = Version("1.1.0-alpha01")
     val AUTOFILL = Version("1.1.0-rc01")
     val BENCHMARK = Version("1.1.0-alpha02")
-    val BIOMETRIC = Version("1.1.0-rc01")
+    val BIOMETRIC = Version("1.2.0-alpha01")
     val BROWSER = Version("1.3.0-rc01")
     val BUILDSRC_TESTS = Version("1.0.0-alpha01")
     val CAMERA = Version("1.0.0-beta12")
@@ -44,7 +44,7 @@
     val CAR_APP = Version("1.0.0-alpha01")
     val COLLECTION = Version("1.2.0-alpha01")
     val CONTENTPAGER = Version("1.1.0-alpha01")
-    val COMPOSE = Version("1.0.0-alpha07")
+    val COMPOSE = Version("1.0.0-alpha08")
     val CONTENTACCESS = Version("1.0.0-alpha01")
     val COORDINATORLAYOUT = Version("1.2.0-alpha01")
     val CORE = Version("1.5.0-alpha05")
@@ -82,9 +82,9 @@
     val MEDIA2 = Version("1.1.0-rc01")
     val MEDIAROUTER = Version("1.3.0-alpha01")
     val NAVIGATION = Version("2.4.0-alpha01")
-    val NAVIGATION_COMPOSE = Version("1.0.0-alpha02")
+    val NAVIGATION_COMPOSE = Version("1.0.0-alpha03")
     val PAGING = Version("3.0.0-alpha09")
-    val PAGING_COMPOSE = Version("1.0.0-alpha02")
+    val PAGING_COMPOSE = Version("1.0.0-alpha03")
     val PALETTE = Version("1.1.0-alpha01")
     val PRINT = Version("1.1.0-beta01")
     val PERCENTLAYOUT = Version("1.1.0-alpha01")
@@ -134,5 +134,5 @@
     val WINDOW = Version("1.0.0-alpha02")
     val WINDOW_EXTENSIONS = Version("1.0.0-alpha01")
     val WINDOW_SIDECAR = Version("0.1.0-alpha01")
-    val WORK = Version("2.5.0-beta01")
+    val WORK = Version("2.5.0-beta02")
 }
diff --git a/busytown/androidx.sh b/busytown/androidx.sh
index 2b81120..ea9da69 100755
--- a/busytown/androidx.sh
+++ b/busytown/androidx.sh
@@ -8,6 +8,10 @@
 # Run Gradle
 impl/build.sh --no-daemon listTaskOutputs -Pandroidx.validateNoUnrecognizedMessages "$@"
 impl/build.sh allProperties "$@" >/dev/null
+subsets="MAIN COMPOSE FLAN"
+for subset in $subsets; do
+  ANDROIDX_PROJECTS=$subset impl/build.sh tasks >/dev/null
+done
 impl/build.sh --no-daemon buildOnServer -Pandroidx.validateNoUnrecognizedMessages checkExternalLicenses \
     -PverifyUpToDate \
     -Pandroidx.coverageEnabled=true \
diff --git a/busytown/impl/build.sh b/busytown/impl/build.sh
index f5e428d..2a8165a 100755
--- a/busytown/impl/build.sh
+++ b/busytown/impl/build.sh
@@ -30,9 +30,14 @@
 # Confirm the existence of .git dirs. TODO(b/170634430) remove this
 (cd frameworks/support && echo "top commit:" && git log -1)
 
+# determine which subset of projects to include, and be sure to print it if it is specified
+PROJECTS_ARG=""
+if [ "$ANDROIDX_PROJECTS" != "" ]; then
+  PROJECTS_ARG="ANDROIDX_PROJECTS=$ANDROIDX_PROJECTS"
+fi
 # --no-watch-fs disables file system watch, because it does not work on busytown
 # due to our builders using OS that is too old.
-run OUT_DIR=out DIST_DIR=$DIST_DIR ANDROID_HOME=./prebuilts/fullsdk-linux \
+run $PROJECTS_ARG OUT_DIR=out DIST_DIR=$DIST_DIR ANDROID_HOME=./prebuilts/fullsdk-linux \
     frameworks/support/gradlew -p frameworks/support \
     --stacktrace \
     -Pandroidx.summarizeStderr \
diff --git a/camera/camera-camera2-pipe-integration/OWNERS b/camera/camera-camera2-pipe-integration/OWNERS
new file mode 100644
index 0000000..e5e4315
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/OWNERS
@@ -0,0 +1,2 @@
+codelogic@google.com
+sushilnath@google.com
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
index 5b245d2..473559c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
@@ -16,24 +16,22 @@
 package androidx.camera.camera2.pipe.integration
 
 import androidx.camera.core.CameraXConfig
-import androidx.camera.camera2.pipe.integration.impl.CameraPipeCameraFactory
-import androidx.camera.camera2.pipe.integration.impl.CameraPipeDeviceSurfaceManager
-import androidx.camera.camera2.pipe.integration.impl.CameraPipeUseCaseFactory
+import androidx.camera.camera2.pipe.integration.impl.CameraPipeFactory
+import androidx.camera.camera2.pipe.integration.impl.StreamConfigurationMap
+import androidx.camera.camera2.pipe.integration.impl.UseCaseConfigurationMap
 
 /**
- * Convenience class for generating a pre-populated CameraPipe [CameraXConfig].
+ * Convenience class for generating a pre-populated CameraPipe based [CameraXConfig].
  */
 object CameraPipeConfig {
     /**
-     * Creates a [CameraXConfig] containing the default CameraPipe implementation for CameraX.
+     * Creates a [CameraXConfig] containing a default CameraPipe implementation for CameraX.
      */
     fun defaultConfig(): CameraXConfig {
         return CameraXConfig.Builder()
-            .setCameraFactoryProvider(::CameraPipeCameraFactory)
-            .setDeviceSurfaceManagerProvider(::CameraPipeDeviceSurfaceManager)
-            .setUseCaseConfigFactoryProvider(::CameraPipeUseCaseFactory)
+            .setCameraFactoryProvider(::CameraPipeFactory)
+            .setDeviceSurfaceManagerProvider(::StreamConfigurationMap)
+            .setUseCaseConfigFactoryProvider(::UseCaseConfigurationMap)
             .build()
     }
-
-    // TODO: Add CameraPipeConfig.Builder for passing options to CameraPipe
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraAdaptor.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraAdaptor.kt
new file mode 100644
index 0000000..3d24397
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraAdaptor.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.integration.impl
+
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.CameraInternal
+import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.Quirks
+import androidx.camera.core.impl.utils.futures.Futures
+import com.google.common.util.concurrent.ListenableFuture
+
+/**
+ * Adapt the [CameraInternal] class to one or more [CameraPipe] based Camera instances.
+ */
+class CameraAdaptor(
+    private val cameraPipe: CameraPipe,
+    private val cameraId: CameraId
+) : CameraInternal {
+
+    init {
+        debug { "Created CameraAdaptor from $cameraPipe for $cameraId" }
+        // TODO: Consider preloading the list of camera ids and metadata.
+    }
+
+    // Load / unload methods
+    override fun open() {
+        TODO("Not yet implemented")
+    }
+
+    override fun close() {
+        TODO("Not yet implemented")
+    }
+
+    override fun release(): ListenableFuture<Void> {
+        // TODO: Determine what the correct way to invoke release is.
+        return Futures.immediateFuture(null)
+    }
+
+    // Static properties of this camera
+    override fun getCameraInfoInternal(): CameraInfoInternal {
+        TODO("Not yet implemented")
+    }
+
+    override fun getCameraQuirks(): Quirks {
+        TODO("Not yet implemented")
+    }
+
+    // Controls for interacting with or observing the state of the camera.
+    override fun getCameraState(): Observable<CameraInternal.State> {
+        TODO("Not yet implemented")
+    }
+
+    override fun getCameraControlInternal(): CameraControlInternal {
+        TODO("Not yet implemented")
+    }
+
+    // UseCase attach / detach behaviors.
+    override fun attachUseCases(useCases: MutableCollection<UseCase>) {
+        TODO("Not yet implemented")
+    }
+
+    override fun detachUseCases(useCases: MutableCollection<UseCase>) {
+        TODO("Not yet implemented")
+    }
+
+    // UseCase state callbacks
+    override fun onUseCaseActive(useCase: UseCase) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onUseCaseUpdated(useCase: UseCase) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onUseCaseReset(useCase: UseCase) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onUseCaseInactive(useCase: UseCase) {
+        TODO("Not yet implemented")
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeCameraFactory.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeCameraFactory.kt
deleted file mode 100644
index d701a51..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeCameraFactory.kt
+++ /dev/null
@@ -1,59 +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.integration.impl
-
-import android.content.Context
-import android.util.Log
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.CameraInternal
-import androidx.camera.core.impl.CameraThreadConfig
-
-/**
- * CameraPipe implementation of CameraX CameraFactory.
- * @constructor Creates a CameraPipeCameraFactory from the provided [Context] and
- * [CameraThreadConfig].
- */
-class CameraPipeCameraFactory(context: Context, threadConfig: CameraThreadConfig) : CameraFactory {
-    companion object {
-        private const val TAG = "CameraPipeCameraFactory"
-        private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
-    }
-
-    private val cameraPipe: CameraPipe = CameraPipe(CameraPipe.Config(context))
-
-    init {
-        if (DEBUG) {
-            Log.d(
-                TAG,
-                "Initialized CameraFactory [Context: $context, " +
-                    "ThreadConfig: $threadConfig, CameraPipe: $cameraPipe]"
-            )
-        }
-    }
-
-    override fun getCamera(cameraId: String): CameraInternal {
-        TODO("Not implemented.")
-    }
-
-    override fun getAvailableCameraIds(): Set<String> {
-        return emptySet()
-    }
-
-    override fun getCameraManager(): Any? {
-        return null
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeDeviceSurfaceManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeDeviceSurfaceManager.kt
deleted file mode 100644
index 4c06e7c..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeDeviceSurfaceManager.kt
+++ /dev/null
@@ -1,68 +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.integration.impl
-
-import android.content.Context
-import android.util.Log
-import android.util.Size
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.core.impl.CameraDeviceSurfaceManager
-import androidx.camera.core.impl.SurfaceConfig
-import androidx.camera.core.impl.UseCaseConfig
-
-/**
- * Provide the guaranteed supported stream capabilities provided by CameraPipe.
- * @constructor Creates a CameraPipeDeviceSurfaceManager from the provided [Context].
- */
-class CameraPipeDeviceSurfaceManager(context: Context, cameraManager: Any?) :
-    CameraDeviceSurfaceManager {
-    companion object {
-        private const val TAG = "CameraPipeSurfaceMgr"
-        private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
-    }
-
-    private val cameraPipe: CameraPipe = CameraPipe(CameraPipe.Config(context))
-
-    init {
-        if (DEBUG) {
-            Log.d(
-                TAG,
-                "Initialized CameraDeviceSurfaceManager [Context: $context, CameraManager:" +
-                    " $cameraManager, CameraPipe: $cameraPipe]"
-            )
-        }
-    }
-
-    override fun checkSupported(cameraId: String, surfaceConfigList: List<SurfaceConfig>): Boolean {
-        return false
-    }
-
-    override fun transformSurfaceConfig(
-        cameraId: String,
-        imageFormat: Int,
-        size: Size
-    ): SurfaceConfig? {
-        TODO("Not implemented.")
-    }
-
-    override fun getSuggestedResolutions(
-        cameraId: String,
-        existingSurfaces: List<SurfaceConfig>,
-        newUseCaseConfigs: List<UseCaseConfig<*>?>
-    ): Map<UseCaseConfig<*>, Size> {
-        TODO("Not implemented.")
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeFactory.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeFactory.kt
new file mode 100644
index 0000000..071da5d
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeFactory.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.integration.impl
+
+import android.content.Context
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraPipe
+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.core.impl.CameraFactory
+import androidx.camera.core.impl.CameraInternal
+import androidx.camera.core.impl.CameraThreadConfig
+
+/**
+ * The CameraPipeCameraFactory is responsible for creating and configuring CameraPipe for CameraX.
+ */
+class CameraPipeFactory(
+    context: Context,
+    threadConfig: CameraThreadConfig
+) : CameraFactory {
+    // Lazily create and configure a CameraPipe instance.
+    private val cameraPipe: CameraPipe by lazy {
+        Debug.traceStart { "CameraPipeCameraFactory#cameraPipe" }
+        val result: CameraPipe?
+        val start = Timestamps.now()
+
+        // TODO: CameraPipe should find a way to make sure callbacks are executed on the configured
+        //   executors that are provided in `threadConfig`
+        debug { "TODO: Use $threadConfig if defined" }
+
+        result = CameraPipe(CameraPipe.Config(appContext = context.applicationContext))
+        debug { "Created CameraPipe in ${start.measureNow().formatMs()}" }
+        Debug.traceStop()
+        result
+    }
+
+    init {
+        debug { "Created CameraPipeCameraFactory" }
+        // TODO: Consider preloading the list of camera ids and metadata.
+    }
+
+    override fun getCamera(cameraId: String): CameraInternal {
+        // TODO: The CameraInternal object is an facade that covers most of the high level camera
+        //   state and interactions. CameraInternal objects are persistent across camera switches.
+
+        return CameraAdaptor(cameraPipe, CameraId(cameraId))
+    }
+
+    override fun getAvailableCameraIds(): Set<String> {
+        // TODO: This may need some amount of work to limit the returned values well behaved "Front"
+        //   and "Back" camera devices.
+        return cameraPipe.cameras().findAll().map { it.value }.toSet()
+    }
+
+    override fun getCameraManager(): Any? {
+        // Note: This object is passed around as an untyped parameter when constructing a few
+        // objects (Such as `DeviceSurfaceManagerProvider`). It's better to rely on the parameter
+        // passing than to try to turn this object into a singleton.
+        return cameraPipe
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeUseCaseFactory.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeUseCaseFactory.kt
deleted file mode 100644
index d3823ab..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeUseCaseFactory.kt
+++ /dev/null
@@ -1,53 +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.integration.impl
-
-import android.content.Context
-import android.util.Log
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.core.impl.Config
-import androidx.camera.core.impl.UseCaseConfigFactory
-
-/**
- * CameraPipe implementation of UseCaseFactory.
- * @constructor Creates a CameraPipeUseCaseFactory from the provided [Context].
- */
-class CameraPipeUseCaseFactory(context: Context) : UseCaseConfigFactory {
-    companion object {
-        private const val TAG = "CameraPipeUseCaseFty"
-        private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
-    }
-
-    private val cameraPipe: CameraPipe = CameraPipe(CameraPipe.Config(context))
-
-    init {
-        if (DEBUG) {
-            Log.d(
-                TAG,
-                "Initialized CameraPipeUseCaseFactory [Context: $context, CameraPipe: $cameraPipe]"
-            )
-        }
-    }
-
-    /**
-     * Returns the configuration for the given capture type, or `null` if the
-     * configuration cannot be produced.
-     */
-    override fun getConfig(captureType: UseCaseConfigFactory.CaptureType): Config? {
-        return null
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StreamConfigurationMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StreamConfigurationMap.kt
new file mode 100644
index 0000000..56b5966
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StreamConfigurationMap.kt
@@ -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.
+ */
+package androidx.camera.camera2.pipe.integration.impl
+
+import android.content.Context
+import android.util.Size
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.core.impl.CameraDeviceSurfaceManager
+import androidx.camera.core.impl.SurfaceConfig
+import androidx.camera.core.impl.UseCaseConfig
+
+/**
+ * Provide utilities for interacting with the set of guaranteed stream combinations.
+ */
+class StreamConfigurationMap(context: Context, cameraManager: Any?) : CameraDeviceSurfaceManager {
+    private val cameraPipe: CameraPipe = cameraManager as CameraPipe
+
+    init {
+        debug { "Created StreamConfigurationMap from $context" }
+    }
+
+    override fun checkSupported(cameraId: String, surfaceConfigList: List<SurfaceConfig>): Boolean {
+        // TODO: This method needs to check to see if the list of SurfaceConfig's is in the map of
+        //   guaranteed stream configurations for this camera's support level.
+        return cameraPipe.cameras().findAll().contains(CameraId(cameraId))
+    }
+
+    override fun transformSurfaceConfig(
+        cameraId: String,
+        imageFormat: Int,
+        size: Size
+    ): SurfaceConfig? {
+        // TODO: Many of the "find a stream combination that will work" is already provided by the
+        //   existing camera2 implementation, and this implementation should leverage that work.
+
+        TODO("Not Implemented")
+    }
+
+    override fun getSuggestedResolutions(
+        cameraId: String,
+        existingSurfaces: List<SurfaceConfig>,
+        newUseCaseConfigs: List<UseCaseConfig<*>?>
+    ): Map<UseCaseConfig<*>, Size> {
+        // TODO: Many of the "find a stream combination that will work" is already provided by the
+        //   existing camera2 implementation, and this implementation should leverage that work.
+
+        TODO("Not Implemented")
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseConfigurationMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseConfigurationMap.kt
new file mode 100644
index 0000000..a0ddab2
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseConfigurationMap.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.integration.impl
+
+import android.content.Context
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.impl.Log.info
+import androidx.camera.core.impl.Config
+import androidx.camera.core.impl.UseCaseConfigFactory
+
+/**
+ * This class builds [Config] objects for a given [UseCaseConfigFactory.CaptureType].
+ *
+ * This includes things like default template and session parameters, as well as maximum resolution
+ * and aspect ratios for the display.
+ */
+class UseCaseConfigurationMap(context: Context) : UseCaseConfigFactory {
+    init {
+        if (context === context.applicationContext) {
+            info {
+                "The provided context ($context) is application scoped and will be used to infer " +
+                    "the default display for computing the default preview size, orientation, " +
+                    "and default aspect ratio for UseCase outputs."
+            }
+        }
+        debug { "Created UseCaseConfigurationMap" }
+    }
+
+    /**
+     * Returns the configuration for the given capture type, or `null` if the configuration
+     * cannot be produced.
+     */
+    override fun getConfig(captureType: UseCaseConfigFactory.CaptureType): Config? {
+        TODO("Not Implemented")
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Result3AStateListener.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Result3AStateListener.kt
new file mode 100644
index 0000000..60f1c45
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Result3AStateListener.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.CaptureResult
+import androidx.annotation.GuardedBy
+import androidx.camera.camera2.pipe.FrameMetadata
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.Result3A
+import androidx.camera.camera2.pipe.Status3A
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+
+/**
+ * Given a map of keys and a list of acceptable values for each key, this checks if the given
+ * [CaptureResult] has all of those keys and for every key the value for that key is one of the
+ * acceptable values.
+ *
+ * This update method can be called multiple times as we get newer [CaptureResult]s from the camera
+ * device. This class also exposes a [Deferred] to query the status of desired state.
+ */
+class Result3AStateListener(
+    private val exitConditionForKeys: Map<CaptureResult.Key<*>, List<Any>>,
+    private val frameLimit: Int? = null,
+    private val timeLimitNs: Long? = null
+) {
+
+    init {
+        require(exitConditionForKeys.isNotEmpty()) { "Exit condition map for keys is empty." }
+    }
+
+    private val deferred = CompletableDeferred<Result3A>()
+
+    @Volatile private var frameNumberOfFirstUpdate: FrameNumber? = null
+    @Volatile private var timestampOfFirstUpdateNs: Long? = null
+    @GuardedBy("this")
+    private var initialRequestNumber: RequestNumber? = null
+
+    fun onRequestSequenceCreated(requestNumber: RequestNumber) {
+        synchronized(this) {
+            if (initialRequestNumber == null) {
+                initialRequestNumber = requestNumber
+            }
+        }
+    }
+
+    fun update(requestNumber: RequestNumber, frameMetadata: FrameMetadata): Boolean {
+        // Save some compute if the task is already complete or has been canceled.
+        if (deferred.isCompleted || deferred.isCancelled) {
+            return true
+        }
+
+        // Ignore the update if the update is from a previously submitted request.
+        synchronized(this) {
+            val initialRequestNumber = initialRequestNumber
+            if (initialRequestNumber == null || requestNumber.value < initialRequestNumber.value) {
+                return false
+            }
+        }
+
+        val currentTimestampNs: Long? = frameMetadata.get(CaptureResult.SENSOR_TIMESTAMP)
+        val currentFrameNumber = frameMetadata.frameNumber
+
+        if (currentTimestampNs != null && timestampOfFirstUpdateNs == null) {
+            timestampOfFirstUpdateNs = currentTimestampNs
+        }
+
+        val timestampOfFirstUpdateNs = timestampOfFirstUpdateNs
+        if (timeLimitNs != null &&
+            timestampOfFirstUpdateNs != null &&
+            currentTimestampNs != null &&
+            currentTimestampNs - timestampOfFirstUpdateNs > timeLimitNs
+        ) {
+            deferred.complete(
+                Result3A(frameMetadata.frameNumber, Status3A.TIME_LIMIT_REACHED)
+            )
+            return true
+        }
+
+        if (frameNumberOfFirstUpdate == null) {
+            frameNumberOfFirstUpdate = currentFrameNumber
+        }
+
+        val frameNumberOfFirstUpdate = frameNumberOfFirstUpdate
+        if (frameNumberOfFirstUpdate != null && frameLimit != null &&
+            currentFrameNumber.value - frameNumberOfFirstUpdate.value > frameLimit
+        ) {
+            deferred.complete(
+                Result3A(frameMetadata.frameNumber, Status3A.FRAME_LIMIT_REACHED)
+            )
+            return true
+        }
+
+        for ((k, v) in exitConditionForKeys) {
+            val valueInCaptureResult = frameMetadata.get(k)
+            if (!v.contains(valueInCaptureResult)) {
+                return false
+            }
+        }
+        deferred.complete(Result3A(frameMetadata.frameNumber, Status3A.OK))
+        return true
+    }
+
+    fun getDeferredResult(): Deferred<Result3A> {
+        return deferred
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Result3AStateListenerTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Result3AStateListenerTest.kt
new file mode 100644
index 0000000..fac4250
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Result3AStateListenerTest.kt
@@ -0,0 +1,326 @@
+/*
+ * 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.CaptureResult
+import android.os.Build
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.Status3A
+import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(CameraPipeRobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+@OptIn(ExperimentalCoroutinesApi::class)
+class Result3AStateListenerTest {
+    @Test
+    fun testWithNoUpdate() {
+        val listenerForKeys = Result3AStateListener(
+            mapOf(
+                CaptureResult.CONTROL_AF_STATE to
+                    listOf(
+                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+                    )
+            )
+        )
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isFalse()
+    }
+
+    @Test
+    fun testKeyWithUndesirableValueInFrameMetadata() {
+        val listenerForKeys = Result3AStateListener(
+            mapOf(
+                CaptureResult.CONTROL_AF_STATE to
+                    listOf(
+                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+                    )
+            )
+        )
+        listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
+
+        val frameMetadata = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN
+            )
+        )
+
+        listenerForKeys.update(RequestNumber(1), frameMetadata)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isFalse()
+    }
+
+    @Test
+    fun testKeyWithDesirableValueInFrameMetadata() {
+        val listenerForKeys = Result3AStateListener(
+            mapOf(
+                CaptureResult.CONTROL_AF_STATE to
+                    listOf(
+                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+                    )
+            )
+        )
+        listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
+
+        val frameMetadata = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+            )
+        )
+        listenerForKeys.update(RequestNumber(1), frameMetadata)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isTrue()
+    }
+
+    @Test
+    fun testKeyNotPresentInFrameMetadata() {
+        val listenerForKeys = Result3AStateListener(
+            mapOf(
+                CaptureResult.CONTROL_AF_STATE to
+                    listOf(
+                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+                    )
+            )
+        )
+        listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
+
+        val frameMetadata = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED
+            )
+        )
+        listenerForKeys.update(RequestNumber(1), frameMetadata)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isFalse()
+    }
+
+    @Test
+    fun testMultipleKeysWithDesiredValuesInFrameMetadata() {
+        val listenerForKeys = Result3AStateListener(
+            mapOf(
+                CaptureResult.CONTROL_AF_STATE to
+                    listOf(
+                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                        CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
+                    ),
+                CaptureResult.CONTROL_AE_STATE to listOf(CaptureResult.CONTROL_AE_STATE_LOCKED)
+            )
+        )
+        listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
+
+        val frameMetadata = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+            )
+        )
+        listenerForKeys.update(RequestNumber(1), frameMetadata)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isTrue()
+    }
+
+    @Test
+    fun testMultipleKeysWithDesiredValuesInFrameMetadataForASubset() {
+        val listenerForKeys = Result3AStateListener(
+            mapOf(
+                CaptureResult.CONTROL_AF_STATE to
+                    listOf(
+                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                        CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
+                    ),
+                CaptureResult.CONTROL_AE_STATE to listOf(CaptureResult.CONTROL_AE_STATE_LOCKED)
+            )
+        )
+        listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
+
+        val frameMetadata = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+            )
+        )
+        listenerForKeys.update(RequestNumber(1), frameMetadata)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isFalse()
+    }
+
+    @Test
+    fun testMultipleUpdates() {
+        val listenerForKeys = Result3AStateListener(
+            mapOf(
+                CaptureResult.CONTROL_AF_STATE to
+                    listOf(
+                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                        CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
+                    ),
+                CaptureResult.CONTROL_AE_STATE to listOf(CaptureResult.CONTROL_AE_STATE_LOCKED)
+            )
+        )
+        listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
+
+        val frameMetadata = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+            )
+        )
+        listenerForKeys.update(RequestNumber(1), frameMetadata)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isFalse()
+
+        val frameMetadata1 = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+            )
+        )
+        listenerForKeys.update(RequestNumber(1), frameMetadata1)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isTrue()
+    }
+
+    @Test
+    fun testTimeLimit() {
+        val listenerForKeys = Result3AStateListener(
+            exitConditionForKeys = mapOf(
+                CaptureResult.CONTROL_AF_STATE to
+                    listOf(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED)
+            ),
+            timeLimitNs = 1000000000L
+        )
+        listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
+
+        val frameMetadata1 = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                CaptureResult.SENSOR_TIMESTAMP to 400000000L
+            )
+        )
+        listenerForKeys.update(RequestNumber(1), frameMetadata1)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isFalse()
+
+        val frameMetadata2 = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                CaptureResult.SENSOR_TIMESTAMP to 900000000L
+            )
+        )
+        listenerForKeys.update(RequestNumber(1), frameMetadata2)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isFalse()
+
+        val frameMetadata3 = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                CaptureResult.SENSOR_TIMESTAMP to 1500000000L
+            )
+        )
+        listenerForKeys.update(RequestNumber(1), frameMetadata3)
+        val completedDeferred = listenerForKeys.getDeferredResult()
+        assertThat(completedDeferred.isCompleted).isTrue()
+        assertThat(completedDeferred.getCompleted().status).isEqualTo(Status3A.TIME_LIMIT_REACHED)
+    }
+
+    @Test
+    fun testFrameLimit() {
+        val listenerForKeys = Result3AStateListener(
+            exitConditionForKeys = mapOf(
+                CaptureResult.CONTROL_AF_STATE to
+                    listOf(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED)
+            ),
+            frameLimit = 10
+        )
+        listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
+
+        listenerForKeys.onRequestSequenceCreated(RequestNumber(1))
+
+        val frameMetadata1 = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                CaptureResult.SENSOR_TIMESTAMP to 400000000L
+            ),
+            frameNumber = FrameNumber(1)
+        )
+        listenerForKeys.update(RequestNumber(1), frameMetadata1)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isFalse()
+
+        val frameMetadata2 = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                CaptureResult.SENSOR_TIMESTAMP to 900000000L
+            ),
+            frameNumber = FrameNumber(3)
+        )
+        listenerForKeys.update(RequestNumber(1), frameMetadata2)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isFalse()
+
+        val frameMetadata3 = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                CaptureResult.SENSOR_TIMESTAMP to 1500000000L
+            ),
+            frameNumber = FrameNumber(10)
+        )
+        listenerForKeys.update(RequestNumber(1), frameMetadata3)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isFalse()
+
+        val frameMetadata4 = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                CaptureResult.SENSOR_TIMESTAMP to 1700000000L
+            ),
+            frameNumber = FrameNumber(12)
+        )
+        listenerForKeys.update(RequestNumber(1), frameMetadata4)
+        val completedDeferred = listenerForKeys.getDeferredResult()
+
+        assertThat(completedDeferred.isCompleted).isTrue()
+        assertThat(completedDeferred.getCompleted().status).isEqualTo(Status3A.FRAME_LIMIT_REACHED)
+    }
+
+    @Test
+    fun testIgnoreUpdatesFromEarlierRequests() {
+        val listenerForKeys = Result3AStateListener(
+            mapOf(
+                CaptureResult.CONTROL_AF_STATE to
+                    listOf(
+                        CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+                    )
+            )
+        )
+
+        val frameMetadata = FakeFrameMetadata(
+            resultMetadata = mapOf(
+                CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+            )
+        )
+        // The reference request number of not yet set on the listener, so the update will be
+        // ignored.
+        listenerForKeys.update(RequestNumber(1), frameMetadata)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isFalse()
+
+        // Update the reference request number for this listener.
+        listenerForKeys.onRequestSequenceCreated(RequestNumber(3))
+
+        // The update is coming from an earlier request so it will be ignored.
+        listenerForKeys.update(RequestNumber(1), frameMetadata)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isFalse()
+
+        // The update is coming from an earlier request so it will be ignored.
+        listenerForKeys.update(RequestNumber(2), frameMetadata)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isFalse()
+
+        // The update is from the same or later request number so it will be accepted.
+        listenerForKeys.update(RequestNumber(3), frameMetadata)
+        assertThat(listenerForKeys.getDeferredResult().isCompleted).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
index a91db52..1ede692 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
@@ -38,11 +38,12 @@
 import androidx.camera.core.impl.CameraThreadConfig;
 import androidx.camera.core.impl.CameraValidator;
 import androidx.camera.core.impl.UseCaseConfigFactory;
-import androidx.camera.core.impl.quirk.IncompleteCameraListQuirk;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.FutureChain;
 import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.core.internal.compat.quirk.IncompleteCameraListQuirk;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
 import androidx.core.os.HandlerCompat;
 import androidx.core.util.Preconditions;
@@ -582,7 +583,7 @@
                 mCameraRepository.init(mCameraFactory);
 
                 // Only verify the devices might have the b/167201193
-                if (IncompleteCameraListQuirk.isCurrentDeviceAffected()) {
+                if (DeviceQuirks.get(IncompleteCameraListQuirk.class) != null) {
                     // Please ensure only validate the camera at the last of the initialization.
                     CameraValidator.validateCameras(mAppContext, mCameraRepository);
                 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 7d18ab7..2ef71ce 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -781,6 +781,15 @@
          * <p>If not set, resolutions with aspect ratio 4:3 will be considered in higher
          * priority.
          *
+         * <p>For the following devices, the aspect ratio will be forced to
+         * {@link AspectRatio#RATIO_16_9} regardless of the config. On these devices, the
+         * camera HAL produces a preview with a 16:9 aspect ratio regardless of the aspect ratio
+         * of the preview surface.
+         * <ul>
+         *     <li>SM-J710MN, Samsung Galaxy J7 (2016)
+         *     <li>SM-T580, Samsung Galaxy Tab A J7 (2016)
+         * </ul>
+         *
          * @param aspectRatio The desired Preview {@link AspectRatio}
          * @return The current Builder.
          */
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
index 336ca3c..c750f77 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
@@ -43,6 +43,10 @@
             quirks.add(new HuaweiMediaStoreLocationValidationQuirk());
         }
 
+        if (IncompleteCameraListQuirk.load()) {
+            quirks.add(new IncompleteCameraListQuirk());
+        }
+
         return quirks;
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/IncompleteCameraListQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/IncompleteCameraListQuirk.java
similarity index 87%
rename from camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/IncompleteCameraListQuirk.java
rename to camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/IncompleteCameraListQuirk.java
index e63e75b..cfd7225 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/IncompleteCameraListQuirk.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/IncompleteCameraListQuirk.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.camera.core.impl.quirk;
+package androidx.camera.core.internal.compat.quirk;
 
 import android.os.Build;
 
@@ -33,9 +33,6 @@
  */
 public class IncompleteCameraListQuirk implements Quirk {
 
-    private IncompleteCameraListQuirk() {
-    }
-
     /** The devices have b/167201193 occur */
     private static final List<String> KNOWN_AFFECTED_DEVICES =
             new ArrayList<>(Arrays.asList("a5y17lte", "tb-8704x", "a7y17lte", "on7xelte",
@@ -45,11 +42,7 @@
                     "a6plte", "hwtrt-q", "co2_sprout", "h3223", "davinci", "vince", "armor_x5",
                     "a2corelte", "j6lte"));
 
-    /**
-     * @return true if the device might report an incomplete camera id list, otherwise false.
-     */
-    public static boolean isCurrentDeviceAffected() {
+    static boolean load() {
         return KNOWN_AFFECTED_DEVICES.contains(Build.DEVICE.toLowerCase());
     }
-
 }
diff --git a/camera/camera-video/build.gradle b/camera/camera-video/build.gradle
index 7bc5694..36ff47c 100644
--- a/camera/camera-video/build.gradle
+++ b/camera/camera-video/build.gradle
@@ -22,11 +22,41 @@
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
+    id("kotlin-android")
 }
 
 dependencies {
     api(project(":camera:camera-core"))
+    implementation("androidx.core:core:1.1.0")
+    implementation("androidx.concurrent:concurrent-futures:1.0.0")
+    implementation(AUTO_VALUE_ANNOTATIONS)
+
     annotationProcessor(AUTO_VALUE)
+
+    testImplementation 'junit:junit:4.12'
+    testImplementation(KOTLIN_STDLIB)
+    testImplementation(ANDROIDX_TEST_CORE)
+    testImplementation(ANDROIDX_TEST_RUNNER)
+    testImplementation(JUNIT)
+    testImplementation(TRUTH)
+    testImplementation(ROBOLECTRIC)
+    testImplementation(MOCKITO_CORE)
+    testImplementation project(":camera:camera-testing"), {
+        exclude group: "androidx.camera", module: "camera-core"
+    }
+
+    androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+    androidTestImplementation(ANDROIDX_TEST_CORE)
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    androidTestImplementation(ANDROIDX_TEST_RULES)
+    androidTestImplementation(TRUTH)
+    androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it's own MockMaker
+    androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it's own MockMaker
+    androidTestImplementation(project(":camera:camera-testing"))
+    androidTestImplementation(KOTLIN_STDLIB)
+    androidTestImplementation(KOTLIN_COROUTINES_ANDROID)
+    androidTestImplementation(project(":concurrent:concurrent-futures-ktx"))
+    androidTestImplementation(project(":internal-testutils-truth"))
 }
 
 android {
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
new file mode 100644
index 0000000..64d1e7d
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
@@ -0,0 +1,194 @@
+/*
+ * 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.media.AudioFormat
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.invocation.InvocationOnMock
+import java.nio.ByteBuffer
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class AudioEncoderTest {
+
+    companion object {
+        private const val MIME_TYPE = "audio/mp4a-latm"
+        private const val BIT_RATE = 64000
+        private const val SAMPLE_RATE = 44100
+        private const val CHANNEL_COUNT = 1
+    }
+
+    private lateinit var encoder: Encoder
+    private lateinit var encoderCallback: EncoderCallback
+    private lateinit var byteBufferProviderJob: Job
+
+    @Before
+    fun setup() {
+        encoderCallback = Mockito.mock(EncoderCallback::class.java)
+        Mockito.doAnswer { args: InvocationOnMock ->
+            val encodedData: EncodedData = args.getArgument(0)
+            encodedData.close()
+            null
+        }.`when`(encoderCallback).onEncodedData(any())
+
+        encoder = EncoderImpl(
+            CameraXExecutors.ioExecutor(),
+            AudioEncoderConfig.builder()
+                .setMimeType(MIME_TYPE)
+                .setBitrate(BIT_RATE)
+                .setSampleRate(SAMPLE_RATE)
+                .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
+                .setChannelCount(CHANNEL_COUNT)
+                .build()
+        )
+        encoder.setEncoderCallback(encoderCallback, CameraXExecutors.directExecutor())
+
+        // Prepare a fake audio source
+        val byteBuffer = ByteBuffer.allocateDirect(1024)
+        byteBufferProviderJob = GlobalScope.launch(Dispatchers.Default) {
+            while (true) {
+                byteBuffer.rewind()
+                (encoder.input as Encoder.ByteBufferInput).putByteBuffer(byteBuffer)
+                delay(200)
+            }
+        }
+    }
+
+    @After
+    fun tearDown() {
+        encoder.release()
+        byteBufferProviderJob.cancel(null)
+    }
+
+    @Test
+    fun discardInputBufferBeforeStart() {
+        // Act.
+        // Wait a second to receive data
+        Thread.sleep(3000L)
+
+        // Assert.
+        verify(encoderCallback, never()).onEncodedData(any())
+    }
+
+    @Test
+    fun canRestartEncoder() {
+        for (i in 0..3) {
+            // Arrange.
+            clearInvocations(encoderCallback)
+
+            // Act.
+            encoder.start()
+
+            // Assert.
+            val inOrder = inOrder(encoderCallback)
+            inOrder.verify(encoderCallback, timeout(5000L)).onEncodeStart()
+            inOrder.verify(encoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+
+            // Act.
+            encoder.stop()
+
+            // Assert.
+            inOrder.verify(encoderCallback, timeout(5000L)).onEncodeStop()
+        }
+    }
+
+    @Test
+    fun canRestartEncoderImmediately() {
+        // Act.
+        encoder.start()
+        encoder.stop()
+        encoder.start()
+
+        // Assert.
+        verify(encoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+    }
+
+    @Test
+    fun canPauseResumeEncoder() {
+        // Act.
+        encoder.start()
+
+        // Assert.
+        verify(encoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+
+        // Act.
+        encoder.pause()
+
+        // Assert.
+        // Since there is no exact event to know the encoder is paused, wait for a while until no
+        // callback.
+        verify(encoderCallback, noInvocation(3000L, 10000L)).onEncodedData(any())
+
+        // Arrange.
+        clearInvocations(encoderCallback)
+
+        // Act.
+        encoder.start()
+
+        // Assert.
+        verify(encoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+    }
+
+    @Test
+    fun canPauseStopStartEncoder() {
+        // Act.
+        encoder.start()
+
+        // Assert.
+        verify(encoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+
+        // Act.
+        encoder.pause()
+
+        // Assert.
+        // Since there is no exact event to know the encoder is paused, wait for a while until no
+        // callback.
+        verify(encoderCallback, noInvocation(3000L, 10000L)).onEncodedData(any())
+
+        // Act.
+        encoder.stop()
+
+        // Assert.
+        verify(encoderCallback, timeout(5000L)).onEncodeStop()
+
+        // Arrange.
+        clearInvocations(encoderCallback)
+
+        // Act.
+        encoder.start()
+
+        // Assert.
+        verify(encoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+    }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/NoInvocationVerificationMode.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/NoInvocationVerificationMode.kt
new file mode 100644
index 0000000..3ec51ac
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/NoInvocationVerificationMode.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.os.SystemClock
+import org.mockito.exceptions.base.MockitoAssertionError
+import org.mockito.exceptions.base.MockitoException
+import org.mockito.internal.invocation.InvocationMarker
+import org.mockito.internal.invocation.InvocationsFinder
+import org.mockito.internal.verification.VerificationModeFactory
+import org.mockito.internal.verification.api.VerificationData
+import org.mockito.verification.Timeout
+import org.mockito.verification.VerificationMode
+
+private class NoInvocationVerificationMode : VerificationMode {
+
+    private val noInvokeDuration: Long
+    private var invocationCount = -1
+    private var checkNoInvocationStartTime = 0L
+
+    constructor(noInvokeDuration: Long) {
+        if (noInvokeDuration < 0) {
+            throw MockitoException("Negative value is not allowed here")
+        }
+        this.noInvokeDuration = noInvokeDuration
+    }
+
+    override fun verify(data: VerificationData?) {
+        val invocations = data!!.allInvocations
+        val wanted = data.target
+        val actualInvocations = InvocationsFinder.findInvocations(invocations, wanted)
+        val actualCount = actualInvocations.size
+        val currentTime = SystemClock.uptimeMillis()
+
+        if (invocationCount == -1) {
+            invocationCount = actualCount
+            checkNoInvocationStartTime = currentTime
+            throw MockitoAssertionError("The first check for no invocation condition.")
+        } else if (invocationCount == -1 || actualCount > invocationCount) {
+            invocationCount = actualCount
+            checkNoInvocationStartTime = currentTime
+            throw MockitoAssertionError("There is new invocation.")
+        } else if (currentTime - checkNoInvocationStartTime > noInvokeDuration) {
+            InvocationMarker.markVerified(actualInvocations, wanted)
+        } else {
+            throw MockitoAssertionError("Keep monitoring invocation")
+        }
+    }
+
+    override fun description(description: String?): VerificationMode {
+        return VerificationModeFactory.description(this, description)
+    }
+}
+
+fun noInvocation(noInvocationDuration: Long, timeout: Long): Timeout {
+    return Timeout(timeout, NoInvocationVerificationMode(noInvocationDuration))
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
new file mode 100644
index 0000000..8e0613f
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
@@ -0,0 +1,1847 @@
+/*
+ * Copyright (C) 2019 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;
+
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
+import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS;
+import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME;
+import static androidx.camera.core.internal.ThreadConfig.OPTION_BACKGROUND_EXECUTOR;
+import static androidx.camera.core.internal.UseCaseEventConfig.OPTION_USE_CASE_EVENT_CALLBACK;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_AUDIO_BIT_RATE;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_AUDIO_CHANNEL_COUNT;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_AUDIO_MIN_BUFFER_SIZE;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_AUDIO_RECORD_SOURCE;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_AUDIO_SAMPLE_RATE;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_BIT_RATE;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_INTRA_FRAME_INTERVAL;
+import static androidx.camera.video.impl.VideoCaptureLegacyConfig.OPTION_VIDEO_FRAME_RATE;
+
+import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.location.Location;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.CamcorderProfile;
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.media.MediaRecorder.AudioSource;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.provider.MediaStore;
+import android.util.Pair;
+import android.util.Size;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.camera.core.AspectRatio;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.CameraXThreads;
+import androidx.camera.core.Logger;
+import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraInternal;
+import androidx.camera.core.impl.CaptureConfig;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.ConfigProvider;
+import androidx.camera.core.impl.DeferrableSurface;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
+import androidx.camera.core.impl.ImmediateSurface;
+import androidx.camera.core.impl.MutableConfig;
+import androidx.camera.core.impl.MutableOptionsBundle;
+import androidx.camera.core.impl.OptionsBundle;
+import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.internal.ThreadConfig;
+import androidx.camera.core.internal.utils.VideoUtil;
+import androidx.camera.video.impl.VideoCaptureLegacyConfig;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A use case for taking a video.
+ *
+ * <p>This class is designed for simple video capturing. It gives basic configuration of the
+ * recorded video such as resolution and file format.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class VideoCaptureLegacy extends UseCase {
+
+    ////////////////////////////////////////////////////////////////////////////////////////////
+    // [UseCase lifetime constant] - Stays constant for the lifetime of the UseCase. Which means
+    // they could be created in the constructor.
+    ////////////////////////////////////////////////////////////////////////////////////////////
+
+    /**
+     * An unknown error occurred.
+     *
+     * <p>See message parameter in onError callback or log for more details.
+     */
+    public static final int ERROR_UNKNOWN = 0;
+    /**
+     * An error occurred with encoder state, either when trying to change state or when an
+     * unexpected state change occurred.
+     */
+    public static final int ERROR_ENCODER = 1;
+    /** An error with muxer state such as during creation or when stopping. */
+    public static final int ERROR_MUXER = 2;
+    /**
+     * An error indicating start recording was called when video recording is still in progress.
+     */
+    public static final int ERROR_RECORDING_IN_PROGRESS = 3;
+    /**
+     * An error indicating the file saving operations.
+     */
+    public static final int ERROR_FILE_IO = 4;
+    /**
+     * An error indicating this VideoCaptureLegacy is not bound to a camera.
+     */
+    public static final int ERROR_INVALID_CAMERA = 5;
+
+    /**
+     * Provides a static configuration with implementation-agnostic options.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final Defaults DEFAULT_CONFIG = new Defaults();
+    private static final String TAG = "VideoCaptureLegacy";
+    /** Amount of time to wait for dequeuing a buffer from the videoEncoder. */
+    private static final int DEQUE_TIMEOUT_USEC = 10000;
+    /** Android preferred mime type for AVC video. */
+    private static final String VIDEO_MIME_TYPE = "video/avc";
+    private static final String AUDIO_MIME_TYPE = "audio/mp4a-latm";
+    /** Camcorder profiles quality list */
+    private static final int[] CamcorderQuality = {
+            CamcorderProfile.QUALITY_2160P,
+            CamcorderProfile.QUALITY_1080P,
+            CamcorderProfile.QUALITY_720P,
+            CamcorderProfile.QUALITY_480P
+    };
+    /**
+     * Audio encoding
+     *
+     * <p>the result of PCM_8BIT and PCM_FLOAT are not good. Set PCM_16BIT as the first option.
+     */
+    private static final short[] sAudioEncoding = {
+            AudioFormat.ENCODING_PCM_16BIT,
+            AudioFormat.ENCODING_PCM_8BIT,
+            AudioFormat.ENCODING_PCM_FLOAT
+    };
+
+    private final BufferInfo mVideoBufferInfo = new BufferInfo();
+    private final Object mMuxerLock = new Object();
+    private final AtomicBoolean mEndOfVideoStreamSignal = new AtomicBoolean(true);
+    private final AtomicBoolean mEndOfAudioStreamSignal = new AtomicBoolean(true);
+    private final AtomicBoolean mEndOfAudioVideoSignal = new AtomicBoolean(true);
+    private final BufferInfo mAudioBufferInfo = new BufferInfo();
+    /** For record the first sample written time. */
+    private final AtomicBoolean mIsFirstVideoSampleWrite = new AtomicBoolean(false);
+    private final AtomicBoolean mIsFirstAudioSampleWrite = new AtomicBoolean(false);
+
+    ////////////////////////////////////////////////////////////////////////////////////////////
+    // [UseCase attached constant] - Is only valid when the UseCase is attached to a camera.
+    ////////////////////////////////////////////////////////////////////////////////////////////
+
+    /** Thread on which all encoding occurs. */
+    private HandlerThread mVideoHandlerThread;
+    private Handler mVideoHandler;
+    /** Thread on which audio encoding occurs. */
+    private HandlerThread mAudioHandlerThread;
+    private Handler mAudioHandler;
+
+    @NonNull
+    MediaCodec mVideoEncoder;
+    @NonNull
+    private MediaCodec mAudioEncoder;
+    @Nullable
+    private ListenableFuture<Void> mRecordingFuture = null;
+
+    ////////////////////////////////////////////////////////////////////////////////////////////
+    // [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
+    ////////////////////////////////////////////////////////////////////////////////////////////
+
+    /** The muxer that writes the encoding data to file. */
+    @GuardedBy("mMuxerLock")
+    private MediaMuxer mMuxer;
+    private boolean mMuxerStarted = false;
+    /** The index of the video track used by the muxer. */
+    private int mVideoTrackIndex;
+    /** The index of the audio track used by the muxer. */
+    private int mAudioTrackIndex;
+    /** Surface the camera writes to, which the videoEncoder uses as input. */
+    Surface mCameraSurface;
+
+    /** audio raw data */
+    @NonNull
+    private AudioRecord mAudioRecorder;
+    private int mAudioBufferSize;
+    private boolean mIsRecording = false;
+    private int mAudioChannelCount;
+    private int mAudioSampleRate;
+    private int mAudioBitRate;
+    private DeferrableSurface mDeferrableSurface;
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    Uri mSavedVideoUri;
+    private ParcelFileDescriptor mParcelFileDescriptor;
+
+    /**
+     * Creates a new video capture use case from the given configuration.
+     *
+     * @param config for this use case instance
+     */
+    VideoCaptureLegacy(@NonNull VideoCaptureLegacyConfig config) {
+        super(config);
+    }
+
+    /** Creates a {@link MediaFormat} using parameters from the configuration */
+    private static MediaFormat createMediaFormat(VideoCaptureLegacyConfig config, Size resolution) {
+        MediaFormat format =
+                MediaFormat.createVideoFormat(
+                        VIDEO_MIME_TYPE, resolution.getWidth(), resolution.getHeight());
+        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
+        format.setInteger(MediaFormat.KEY_BIT_RATE, config.getBitRate());
+        format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoFrameRate());
+        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, config.getIFrameInterval());
+
+        return format;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public UseCaseConfig<?> getDefaultConfig(boolean applyDefaultConfig,
+            @NonNull UseCaseConfigFactory factory) {
+        Config captureConfig = factory.getConfig(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE);
+
+        if (applyDefaultConfig) {
+            captureConfig = Config.mergeConfigs(captureConfig, DEFAULT_CONFIG.getConfig());
+        }
+
+        return captureConfig == null ? null :
+                getUseCaseConfigBuilder(captureConfig).getUseCaseConfig();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @SuppressWarnings("WrongConstant")
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public void onAttached() {
+        mVideoHandlerThread = new HandlerThread(CameraXThreads.TAG + "video encoding thread");
+        mAudioHandlerThread = new HandlerThread(CameraXThreads.TAG + "audio encoding thread");
+
+        // video thread start
+        mVideoHandlerThread.start();
+        mVideoHandler = new Handler(mVideoHandlerThread.getLooper());
+
+        // audio thread start
+        mAudioHandlerThread.start();
+        mAudioHandler = new Handler(mAudioHandlerThread.getLooper());
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
+        if (mCameraSurface != null) {
+            mVideoEncoder.stop();
+            mVideoEncoder.release();
+            mAudioEncoder.stop();
+            mAudioEncoder.release();
+            releaseCameraSurface(false);
+        }
+
+        try {
+            mVideoEncoder = MediaCodec.createEncoderByType(VIDEO_MIME_TYPE);
+            mAudioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE);
+        } catch (IOException e) {
+            throw new IllegalStateException("Unable to create MediaCodec due to: " + e.getCause());
+        }
+
+        setupEncoder(getCameraId(), suggestedResolution);
+        return suggestedResolution;
+    }
+
+    /**
+     * Starts recording video, which continues until {@link VideoCaptureLegacy#stopRecording()} is
+     * called.
+     *
+     * <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
+     * {@link OnVideoSavedCallback#onError(int, String, Throwable)}.
+     *
+     * @param outputFileOptions Location to save the video capture
+     * @param executor          The executor in which the callback methods will be run.
+     * @param callback          Callback for when the recorded video saving completion or failure.
+     */
+    public void startRecording(
+            @NonNull OutputFileOptions outputFileOptions, @NonNull Executor executor,
+            @NonNull OnVideoSavedCallback callback) {
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            CameraXExecutors.mainThreadExecutor().execute(() -> startRecording(outputFileOptions,
+                    executor, callback));
+            return;
+        }
+        Logger.i(TAG, "startRecording");
+        mIsFirstVideoSampleWrite.set(false);
+        mIsFirstAudioSampleWrite.set(false);
+
+        OnVideoSavedCallback postListener = new VideoSavedListenerWrapper(executor, callback);
+
+        CameraInternal attachedCamera = getCamera();
+        if (attachedCamera == null) {
+            // Not bound. Notify callback.
+            postListener.onError(ERROR_INVALID_CAMERA,
+                    "Not bound to a Camera [" + VideoCaptureLegacy.this + "]", null);
+            return;
+        }
+
+        if (!mEndOfAudioVideoSignal.get()) {
+            postListener.onError(
+                    ERROR_RECORDING_IN_PROGRESS, "It is still in video recording!",
+                    null);
+            return;
+        }
+
+        try {
+            // audioRecord start
+            mAudioRecorder.startRecording();
+        } catch (IllegalStateException e) {
+            postListener.onError(ERROR_ENCODER, "AudioRecorder start fail", e);
+            return;
+        }
+
+        AtomicReference<Completer<Void>> recordingCompleterRef = new AtomicReference<>();
+        mRecordingFuture = CallbackToFutureAdapter.getFuture(
+                completer -> {
+                    recordingCompleterRef.set(completer);
+                    return "startRecording";
+                });
+        Completer<Void> recordingCompleter =
+                Preconditions.checkNotNull(recordingCompleterRef.get());
+
+        mRecordingFuture.addListener(() -> {
+            mRecordingFuture = null;
+            // Do the setup of the videoEncoder at the end of video recording instead of at the
+            // start of recording because it requires attaching a new Surface. This causes a
+            // glitch so we don't want that to incur latency at the start of capture.
+            if (getCamera() != null) {
+                // Ensure the use case is bound. Asynchronous stopping procedure may occur after
+                // the use case is unbound, i.e. after onDetached().
+                setupEncoder(getCameraId(), getAttachedSurfaceResolution());
+                notifyReset();
+            }
+        }, CameraXExecutors.mainThreadExecutor());
+
+        try {
+            // video encoder start
+            Logger.i(TAG, "videoEncoder start");
+            mVideoEncoder.start();
+            // audio encoder start
+            Logger.i(TAG, "audioEncoder start");
+            mAudioEncoder.start();
+
+        } catch (IllegalStateException e) {
+            recordingCompleter.set(null);
+            postListener.onError(ERROR_ENCODER, "Audio/Video encoder start fail", e);
+            return;
+        }
+
+        try {
+            synchronized (mMuxerLock) {
+                mMuxer = initMediaMuxer(outputFileOptions);
+                Preconditions.checkNotNull(mMuxer);
+                mMuxer.setOrientationHint(getRelativeRotation(attachedCamera));
+
+                Metadata metadata = outputFileOptions.getMetadata();
+                if (metadata != null && metadata.location != null) {
+                    mMuxer.setLocation(
+                            (float) metadata.location.getLatitude(),
+                            (float) metadata.location.getLongitude());
+                }
+            }
+        } catch (IOException e) {
+            recordingCompleter.set(null);
+            postListener.onError(ERROR_MUXER, "MediaMuxer creation failed!", e);
+            return;
+        }
+
+        mEndOfVideoStreamSignal.set(false);
+        mEndOfAudioStreamSignal.set(false);
+        mEndOfAudioVideoSignal.set(false);
+        mIsRecording = true;
+
+        notifyActive();
+        mAudioHandler.post(() -> audioEncode(postListener));
+
+        String cameraId = getCameraId();
+        Size resolution = getAttachedSurfaceResolution();
+        mVideoHandler.post(
+                () -> {
+                    boolean errorOccurred = videoEncode(postListener, cameraId, resolution);
+                    if (!errorOccurred) {
+                        postListener.onVideoSaved(new OutputFileResults(mSavedVideoUri));
+                        mSavedVideoUri = null;
+                    }
+                    recordingCompleter.set(null);
+                });
+    }
+
+    /**
+     * Stops recording video, this must be called after {@link
+     * VideoCaptureLegacy#startRecording(OutputFileOptions, Executor, OnVideoSavedCallback)} is
+     * called.
+     *
+     * <p>stopRecording() is asynchronous API. User need to check if {@link
+     * OnVideoSavedCallback#onVideoSaved(OutputFileResults)} or
+     * {@link OnVideoSavedCallback#onError(int, String, Throwable)} be called
+     * before startRecording.
+     */
+    public void stopRecording() {
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            CameraXExecutors.mainThreadExecutor().execute(() -> stopRecording());
+            return;
+        }
+        Logger.i(TAG, "stopRecording");
+        notifyInactive();
+        if (!mEndOfAudioVideoSignal.get() && mIsRecording) {
+            // stop audio encoder thread, and wait video encoder and muxer stop.
+            mEndOfAudioStreamSignal.set(true);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void onDetached() {
+        stopRecording();
+
+        if (mRecordingFuture != null) {
+            mRecordingFuture.addListener(() -> releaseResources(),
+                    CameraXExecutors.mainThreadExecutor());
+        } else {
+            releaseResources();
+        }
+    }
+
+    private void releaseResources() {
+        mVideoHandlerThread.quitSafely();
+
+        // audio encoder release
+        mAudioHandlerThread.quitSafely();
+        if (mAudioEncoder != null) {
+            mAudioEncoder.release();
+            mAudioEncoder = null;
+        }
+
+        if (mAudioRecorder != null) {
+            mAudioRecorder.release();
+            mAudioRecorder = null;
+        }
+
+        if (mCameraSurface != null) {
+            releaseCameraSurface(true);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public UseCaseConfig.Builder<?, ?, ?> getUseCaseConfigBuilder(@NonNull Config config) {
+        return Builder.fromConfig(config);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @UiThread
+    @Override
+    public void onStateDetached() {
+        stopRecording();
+    }
+
+    @UiThread
+    private void releaseCameraSurface(final boolean releaseVideoEncoder) {
+        if (mDeferrableSurface == null) {
+            return;
+        }
+
+        final MediaCodec videoEncoder = mVideoEncoder;
+
+        // Calling close should allow termination future to complete and close the surface with
+        // the listener that was added after constructing the DeferrableSurface.
+        mDeferrableSurface.close();
+        mDeferrableSurface.getTerminationFuture().addListener(
+                () -> {
+                    if (releaseVideoEncoder && videoEncoder != null) {
+                        videoEncoder.release();
+                    }
+                }, CameraXExecutors.mainThreadExecutor());
+
+        if (releaseVideoEncoder) {
+            mVideoEncoder = null;
+        }
+        mCameraSurface = null;
+        mDeferrableSurface = null;
+    }
+
+
+    /**
+     * Sets the desired rotation of the output video.
+     *
+     * <p>In most cases this should be set to the current rotation returned by {@link
+     * Display#getRotation()}.
+     *
+     * @param rotation Desired rotation of the output video.
+     */
+    public void setTargetRotation(@RotationValue int rotation) {
+        setTargetRotationInternal(rotation);
+    }
+
+    /**
+     * Setup the {@link MediaCodec} for encoding video from a camera {@link Surface} and encoding
+     * audio from selected audio source.
+     */
+    @UiThread
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    void setupEncoder(@NonNull String cameraId, @NonNull Size resolution) {
+        VideoCaptureLegacyConfig config = (VideoCaptureLegacyConfig) getCurrentConfig();
+
+        // video encoder setup
+        mVideoEncoder.reset();
+        mVideoEncoder.configure(
+                createMediaFormat(config, resolution), /*surface*/
+                null, /*crypto*/
+                null,
+                MediaCodec.CONFIGURE_FLAG_ENCODE);
+        if (mCameraSurface != null) {
+            releaseCameraSurface(false);
+        }
+        Surface cameraSurface = mVideoEncoder.createInputSurface();
+        mCameraSurface = cameraSurface;
+
+        SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
+
+        if (mDeferrableSurface != null) {
+            mDeferrableSurface.close();
+        }
+        mDeferrableSurface = new ImmediateSurface(mCameraSurface);
+        mDeferrableSurface.getTerminationFuture().addListener(
+                cameraSurface::release, CameraXExecutors.mainThreadExecutor()
+        );
+
+        sessionConfigBuilder.addSurface(mDeferrableSurface);
+
+        sessionConfigBuilder.addErrorListener(new SessionConfig.ErrorListener() {
+            @Override
+            public void onError(@NonNull SessionConfig sessionConfig,
+                    @NonNull SessionConfig.SessionError error) {
+                // Ensure the attached camera has not changed before calling setupEncoder.
+                // TODO(b/143915543): Ensure this never gets called by a camera that is not attached
+                //  to this use case so we don't need to do this check.
+                if (isCurrentCamera(cameraId)) {
+                    // Only reset the pipeline when the bound camera is the same.
+                    setupEncoder(cameraId, resolution);
+                }
+            }
+        });
+
+        updateSessionConfig(sessionConfigBuilder.build());
+
+        // audio encoder setup
+        setAudioParametersByCamcorderProfile(resolution, cameraId);
+        mAudioEncoder.reset();
+        mAudioEncoder.configure(
+                createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+
+        if (mAudioRecorder != null) {
+            mAudioRecorder.release();
+        }
+        mAudioRecorder = autoConfigAudioRecordSource(config);
+        // check mAudioRecorder
+        if (mAudioRecorder == null) {
+            Logger.e(TAG, "AudioRecord object cannot initialized correctly!");
+        }
+
+        mVideoTrackIndex = -1;
+        mAudioTrackIndex = -1;
+        mIsRecording = false;
+    }
+
+    /**
+     * Write a buffer that has been encoded to file.
+     *
+     * @param bufferIndex the index of the buffer in the videoEncoder that has available data
+     * @return returns true if this buffer is the end of the stream
+     */
+    private boolean writeVideoEncodedBuffer(int bufferIndex) {
+        if (bufferIndex < 0) {
+            Logger.e(TAG, "Output buffer should not have negative index: " + bufferIndex);
+            return false;
+        }
+        // Get data from buffer
+        ByteBuffer outputBuffer = mVideoEncoder.getOutputBuffer(bufferIndex);
+
+        // Check if buffer is valid, if not then return
+        if (outputBuffer == null) {
+            Logger.d(TAG, "OutputBuffer was null.");
+            return false;
+        }
+
+        // Write data to mMuxer if available
+        if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0 && mVideoBufferInfo.size > 0) {
+            outputBuffer.position(mVideoBufferInfo.offset);
+            outputBuffer.limit(mVideoBufferInfo.offset + mVideoBufferInfo.size);
+            mVideoBufferInfo.presentationTimeUs = (System.nanoTime() / 1000);
+
+            synchronized (mMuxerLock) {
+                if (!mIsFirstVideoSampleWrite.get()) {
+                    Logger.i(TAG, "First video sample written.");
+                    mIsFirstVideoSampleWrite.set(true);
+                }
+                mMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, mVideoBufferInfo);
+            }
+        }
+
+        // Release data
+        mVideoEncoder.releaseOutputBuffer(bufferIndex, false);
+
+        // Return true if EOS is set
+        return (mVideoBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+    }
+
+    private boolean writeAudioEncodedBuffer(int bufferIndex) {
+        ByteBuffer buffer = getOutputBuffer(mAudioEncoder, bufferIndex);
+        buffer.position(mAudioBufferInfo.offset);
+        if (mAudioTrackIndex >= 0
+                && mVideoTrackIndex >= 0
+                && mAudioBufferInfo.size > 0
+                && mAudioBufferInfo.presentationTimeUs > 0) {
+            try {
+                synchronized (mMuxerLock) {
+                    if (!mIsFirstAudioSampleWrite.get()) {
+                        Logger.i(TAG, "First audio sample written.");
+                        mIsFirstAudioSampleWrite.set(true);
+                    }
+                    mMuxer.writeSampleData(mAudioTrackIndex, buffer, mAudioBufferInfo);
+                }
+            } catch (Exception e) {
+                Logger.e(
+                        TAG,
+                        "audio error:size="
+                                + mAudioBufferInfo.size
+                                + "/offset="
+                                + mAudioBufferInfo.offset
+                                + "/timeUs="
+                                + mAudioBufferInfo.presentationTimeUs);
+                e.printStackTrace();
+            }
+        }
+        mAudioEncoder.releaseOutputBuffer(bufferIndex, false);
+        return (mAudioBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+    }
+
+    /**
+     * Encoding which runs indefinitely until end of stream is signaled. This should not run on the
+     * main thread otherwise it will cause the application to block.
+     *
+     * @return returns {@code true} if an error condition occurred, otherwise returns {@code false}
+     */
+    boolean videoEncode(@NonNull OnVideoSavedCallback videoSavedCallback, @NonNull String cameraId,
+            @NonNull Size resolution) {
+        // Main encoding loop. Exits on end of stream.
+        boolean errorOccurred = false;
+        boolean videoEos = false;
+        while (!videoEos && !errorOccurred) {
+            // Check for end of stream from main thread
+            if (mEndOfVideoStreamSignal.get()) {
+                mVideoEncoder.signalEndOfInputStream();
+                mEndOfVideoStreamSignal.set(false);
+            }
+
+            // Deque buffer to check for processing step
+            int outputBufferId =
+                    mVideoEncoder.dequeueOutputBuffer(mVideoBufferInfo, DEQUE_TIMEOUT_USEC);
+            switch (outputBufferId) {
+                case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
+                    if (mMuxerStarted) {
+                        videoSavedCallback.onError(
+                                ERROR_ENCODER,
+                                "Unexpected change in video encoding format.",
+                                null);
+                        errorOccurred = true;
+                    }
+
+                    synchronized (mMuxerLock) {
+                        mVideoTrackIndex = mMuxer.addTrack(mVideoEncoder.getOutputFormat());
+                        if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0) {
+                            mMuxerStarted = true;
+                            Logger.i(TAG, "media mMuxer start");
+                            mMuxer.start();
+                        }
+                    }
+                    break;
+                case MediaCodec.INFO_TRY_AGAIN_LATER:
+                    // Timed out. Just wait until next attempt to deque.
+                    break;
+                default:
+                    videoEos = writeVideoEncodedBuffer(outputBufferId);
+            }
+        }
+
+        try {
+            Logger.i(TAG, "videoEncoder stop");
+            mVideoEncoder.stop();
+        } catch (IllegalStateException e) {
+            videoSavedCallback.onError(ERROR_ENCODER,
+                    "Video encoder stop failed!", e);
+            errorOccurred = true;
+        }
+
+        try {
+            // new MediaMuxer instance required for each new file written, and release current one.
+            synchronized (mMuxerLock) {
+                if (mMuxer != null) {
+                    if (mMuxerStarted) {
+                        mMuxer.stop();
+                    }
+                    mMuxer.release();
+                    mMuxer = null;
+                }
+            }
+        } catch (IllegalStateException e) {
+            videoSavedCallback.onError(ERROR_MUXER, "Muxer stop failed!", e);
+            errorOccurred = true;
+        }
+
+        if (mParcelFileDescriptor != null) {
+            try {
+                mParcelFileDescriptor.close();
+                mParcelFileDescriptor = null;
+            } catch (IOException e) {
+                videoSavedCallback.onError(ERROR_MUXER, "File descriptor close failed!", e);
+                errorOccurred = true;
+            }
+        }
+
+        mMuxerStarted = false;
+
+        // notify the UI thread that the video recording has finished
+        mEndOfAudioVideoSignal.set(true);
+
+        Logger.i(TAG, "Video encode thread end.");
+        return errorOccurred;
+    }
+
+    boolean audioEncode(OnVideoSavedCallback videoSavedCallback) {
+        // Audio encoding loop. Exits on end of stream.
+        boolean audioEos = false;
+        int outIndex;
+        while (!audioEos && mIsRecording) {
+            // Check for end of stream from main thread
+            if (mEndOfAudioStreamSignal.get()) {
+                mEndOfAudioStreamSignal.set(false);
+                mIsRecording = false;
+            }
+
+            // get audio deque input buffer
+            if (mAudioEncoder != null && mAudioRecorder != null) {
+                int index = mAudioEncoder.dequeueInputBuffer(-1);
+                if (index >= 0) {
+                    final ByteBuffer buffer = getInputBuffer(mAudioEncoder, index);
+                    buffer.clear();
+                    int length = mAudioRecorder.read(buffer, mAudioBufferSize);
+                    if (length > 0) {
+                        mAudioEncoder.queueInputBuffer(
+                                index,
+                                0,
+                                length,
+                                (System.nanoTime() / 1000),
+                                mIsRecording ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+                    }
+                }
+
+                // start to dequeue audio output buffer
+                do {
+                    outIndex = mAudioEncoder.dequeueOutputBuffer(mAudioBufferInfo, 0);
+                    switch (outIndex) {
+                        case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
+                            synchronized (mMuxerLock) {
+                                mAudioTrackIndex = mMuxer.addTrack(mAudioEncoder.getOutputFormat());
+                                if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0) {
+                                    mMuxerStarted = true;
+                                    mMuxer.start();
+                                }
+                            }
+                            break;
+                        case MediaCodec.INFO_TRY_AGAIN_LATER:
+                            break;
+                        default:
+                            audioEos = writeAudioEncodedBuffer(outIndex);
+                    }
+                } while (outIndex >= 0 && !audioEos); // end of dequeue output buffer
+            }
+        } // end of while loop
+
+        // Audio Stop
+        try {
+            Logger.i(TAG, "audioRecorder stop");
+            mAudioRecorder.stop();
+        } catch (IllegalStateException e) {
+            videoSavedCallback.onError(
+                    ERROR_ENCODER, "Audio recorder stop failed!", e);
+        }
+
+        try {
+            mAudioEncoder.stop();
+        } catch (IllegalStateException e) {
+            videoSavedCallback.onError(ERROR_ENCODER,
+                    "Audio encoder stop failed!", e);
+        }
+
+        Logger.i(TAG, "Audio encode thread end");
+        // Use AtomicBoolean to signal because MediaCodec.signalEndOfInputStream() is not thread
+        // safe
+        mEndOfVideoStreamSignal.set(true);
+
+        return false;
+    }
+
+    private ByteBuffer getInputBuffer(MediaCodec codec, int index) {
+        return codec.getInputBuffer(index);
+    }
+
+    private ByteBuffer getOutputBuffer(MediaCodec codec, int index) {
+        return codec.getOutputBuffer(index);
+    }
+
+    /** Creates a {@link MediaFormat} using parameters for audio from the configuration */
+    private MediaFormat createAudioMediaFormat() {
+        MediaFormat format =
+                MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, mAudioSampleRate,
+                        mAudioChannelCount);
+        format.setInteger(
+                MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
+        format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitRate);
+
+        return format;
+    }
+
+    /** Create a AudioRecord object to get raw data */
+    private AudioRecord autoConfigAudioRecordSource(VideoCaptureLegacyConfig config) {
+        for (short audioFormat : sAudioEncoding) {
+
+            // Use channel count to determine stereo vs mono
+            int channelConfig =
+                    mAudioChannelCount == 1
+                            ? AudioFormat.CHANNEL_IN_MONO
+                            : AudioFormat.CHANNEL_IN_STEREO;
+            int source = config.getAudioRecordSource();
+
+            try {
+                int bufferSize =
+                        AudioRecord.getMinBufferSize(mAudioSampleRate, channelConfig, audioFormat);
+
+                if (bufferSize <= 0) {
+                    bufferSize = config.getAudioMinBufferSize();
+                }
+
+                AudioRecord recorder =
+                        new AudioRecord(
+                                source,
+                                mAudioSampleRate,
+                                channelConfig,
+                                audioFormat,
+                                bufferSize * 2);
+
+                if (recorder.getState() == AudioRecord.STATE_INITIALIZED) {
+                    mAudioBufferSize = bufferSize;
+                    Logger.i(
+                            TAG,
+                            "source: "
+                                    + source
+                                    + " audioSampleRate: "
+                                    + mAudioSampleRate
+                                    + " channelConfig: "
+                                    + channelConfig
+                                    + " audioFormat: "
+                                    + audioFormat
+                                    + " bufferSize: "
+                                    + bufferSize);
+                    return recorder;
+                }
+            } catch (Exception e) {
+                Logger.e(TAG, "Exception, keep trying.", e);
+            }
+        }
+
+        return null;
+    }
+
+    /** Set audio record parameters by CamcorderProfile */
+    private void setAudioParametersByCamcorderProfile(Size currentResolution, String cameraId) {
+        CamcorderProfile profile;
+        boolean isCamcorderProfileFound = false;
+
+        try {
+            for (int quality : CamcorderQuality) {
+                if (CamcorderProfile.hasProfile(Integer.parseInt(cameraId), quality)) {
+                    profile = CamcorderProfile.get(Integer.parseInt(cameraId), quality);
+                    if (currentResolution.getWidth() == profile.videoFrameWidth
+                            && currentResolution.getHeight() == profile.videoFrameHeight) {
+                        mAudioChannelCount = profile.audioChannels;
+                        mAudioSampleRate = profile.audioSampleRate;
+                        mAudioBitRate = profile.audioBitRate;
+                        isCamcorderProfileFound = true;
+                        break;
+                    }
+                }
+            }
+        } catch (NumberFormatException e) {
+            Logger.i(TAG, "The camera Id is not an integer because the camera may be a removable "
+                    + "device. Use the default values for the audio related settings.");
+        }
+
+        // In case no corresponding camcorder profile can be founded, * get default value from
+        // VideoCaptureConfig.
+        if (!isCamcorderProfileFound) {
+            VideoCaptureLegacyConfig config = (VideoCaptureLegacyConfig) getCurrentConfig();
+            mAudioChannelCount = config.getAudioChannelCount();
+            mAudioSampleRate = config.getAudioSampleRate();
+            mAudioBitRate = config.getAudioBitRate();
+        }
+    }
+
+    @SuppressLint("UnsafeNewApiCall")
+    @NonNull
+    private MediaMuxer initMediaMuxer(@NonNull OutputFileOptions outputFileOptions)
+            throws IOException {
+        MediaMuxer mediaMuxer;
+
+        if (outputFileOptions.isSavingToFile()) {
+            File savedVideoFile = outputFileOptions.getFile();
+            mSavedVideoUri = Uri.fromFile(outputFileOptions.getFile());
+
+            mediaMuxer = new MediaMuxer(savedVideoFile.getAbsolutePath(),
+                    MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+        } else if (outputFileOptions.isSavingToFileDescriptor()) {
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+                throw new IllegalArgumentException("Using a FileDescriptor to record a video is "
+                        + "only supported for Android 8.0 or above.");
+            }
+
+            mediaMuxer = new MediaMuxer(outputFileOptions.getFileDescriptor(),
+                    MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+        } else if (outputFileOptions.isSavingToMediaStore()) {
+            ContentValues values = outputFileOptions.getContentValues() != null
+                    ? new ContentValues(outputFileOptions.getContentValues())
+                    : new ContentValues();
+
+            mSavedVideoUri = outputFileOptions.getContentResolver().insert(
+                    outputFileOptions.getSaveCollection(), values);
+
+            if (mSavedVideoUri == null) {
+                throw new IOException("Invalid Uri!");
+            }
+
+            // Sine API 26, media muxer could be initiated by a FileDescriptor.
+            try {
+                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+                    String savedLocationPath = VideoUtil.getAbsolutePathFromUri(
+                            outputFileOptions.getContentResolver(), mSavedVideoUri);
+
+                    Logger.i(TAG, "Saved Location Path: " + savedLocationPath);
+                    mediaMuxer = new MediaMuxer(savedLocationPath,
+                            MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+                } else {
+                    mParcelFileDescriptor =
+                            outputFileOptions.getContentResolver().openFileDescriptor(
+                                    mSavedVideoUri, "rw");
+                    mediaMuxer = new MediaMuxer(mParcelFileDescriptor.getFileDescriptor(),
+                            MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+                }
+            } catch (IOException e) {
+                mSavedVideoUri = null;
+                throw e;
+            }
+        } else {
+            throw new IllegalArgumentException(
+                    "The OutputFileOptions should assign before recording");
+        }
+
+        return mediaMuxer;
+    }
+
+    /**
+     * Describes the error that occurred during video capture operations.
+     *
+     * <p>This is a parameter sent to the error callback functions set in listeners such as {@link
+     * OnVideoSavedCallback#onError(int, String, Throwable)}.
+     *
+     * <p>See message parameter in onError callback or log for more details.
+     *
+     * @hide
+     */
+    @IntDef({ERROR_UNKNOWN, ERROR_ENCODER, ERROR_MUXER, ERROR_RECORDING_IN_PROGRESS,
+            ERROR_FILE_IO, ERROR_INVALID_CAMERA})
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public @interface VideoCaptureError {
+    }
+
+    /** Listener containing callbacks for video file I/O events. */
+    public interface OnVideoSavedCallback {
+        /** Called when the video has been successfully saved. */
+        void onVideoSaved(@NonNull OutputFileResults outputFileResults);
+
+        /** Called when an error occurs while attempting to save the video. */
+        void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
+                @Nullable Throwable cause);
+    }
+
+    /**
+     * Provides a base static default configuration for the VideoCapture
+     *
+     * <p>These values may be overridden by the implementation. They only provide a minimum set of
+     * defaults that are implementation independent.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final class Defaults
+            implements ConfigProvider<VideoCaptureLegacyConfig> {
+        private static final int DEFAULT_VIDEO_FRAME_RATE = 30;
+        /** 8Mb/s the recommend rate for 30fps 1080p */
+        private static final int DEFAULT_BIT_RATE = 8 * 1024 * 1024;
+        /** Seconds between each key frame */
+        private static final int DEFAULT_INTRA_FRAME_INTERVAL = 1;
+        /** audio bit rate */
+        private static final int DEFAULT_AUDIO_BIT_RATE = 64000;
+        /** audio sample rate */
+        private static final int DEFAULT_AUDIO_SAMPLE_RATE = 8000;
+        /** audio channel count */
+        private static final int DEFAULT_AUDIO_CHANNEL_COUNT = 1;
+        /** audio record source */
+        private static final int DEFAULT_AUDIO_RECORD_SOURCE = AudioSource.MIC;
+        /** audio default minimum buffer size */
+        private static final int DEFAULT_AUDIO_MIN_BUFFER_SIZE = 1024;
+        /** Current max resolution of VideoCaptureLegacy is set as FHD */
+        private static final Size DEFAULT_MAX_RESOLUTION = new Size(1920, 1080);
+        /** Surface occupancy prioirty to this use case */
+        private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 3;
+        private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_16_9;
+
+        private static final VideoCaptureLegacyConfig DEFAULT_CONFIG;
+
+        static {
+            Builder builder = new Builder()
+                    .setVideoFrameRate(DEFAULT_VIDEO_FRAME_RATE)
+                    .setBitRate(DEFAULT_BIT_RATE)
+                    .setIFrameInterval(DEFAULT_INTRA_FRAME_INTERVAL)
+                    .setAudioBitRate(DEFAULT_AUDIO_BIT_RATE)
+                    .setAudioSampleRate(DEFAULT_AUDIO_SAMPLE_RATE)
+                    .setAudioChannelCount(DEFAULT_AUDIO_CHANNEL_COUNT)
+                    .setAudioRecordSource(DEFAULT_AUDIO_RECORD_SOURCE)
+                    .setAudioMinBufferSize(DEFAULT_AUDIO_MIN_BUFFER_SIZE)
+                    .setMaxResolution(DEFAULT_MAX_RESOLUTION)
+                    .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
+                    .setTargetAspectRatio(DEFAULT_ASPECT_RATIO);
+
+            DEFAULT_CONFIG = builder.getUseCaseConfig();
+        }
+
+        @NonNull
+        @Override
+        public VideoCaptureLegacyConfig getConfig() {
+            return DEFAULT_CONFIG;
+        }
+    }
+
+    /** Holder class for metadata that should be saved alongside captured video. */
+    public static final class Metadata {
+        /** Data representing a geographic location. */
+        @Nullable
+        public Location location;
+    }
+
+    private static final class VideoSavedListenerWrapper implements OnVideoSavedCallback {
+
+        @NonNull
+        Executor mExecutor;
+        @NonNull
+        OnVideoSavedCallback mOnVideoSavedCallback;
+
+        VideoSavedListenerWrapper(@NonNull Executor executor,
+                @NonNull OnVideoSavedCallback onVideoSavedCallback) {
+            mExecutor = executor;
+            mOnVideoSavedCallback = onVideoSavedCallback;
+        }
+
+        @Override
+        public void onVideoSaved(@NonNull OutputFileResults outputFileResults) {
+            try {
+                mExecutor.execute(() -> mOnVideoSavedCallback.onVideoSaved(outputFileResults));
+            } catch (RejectedExecutionException e) {
+                Logger.e(TAG, "Unable to post to the supplied executor.");
+            }
+        }
+
+        @Override
+        public void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
+                @Nullable Throwable cause) {
+            try {
+                mExecutor.execute(
+                        () -> mOnVideoSavedCallback.onError(videoCaptureError, message, cause));
+            } catch (RejectedExecutionException e) {
+                Logger.e(TAG, "Unable to post to the supplied executor.");
+            }
+        }
+
+    }
+
+    /** Builder for a {@link VideoCaptureLegacy}. */
+    public static final class Builder
+            implements
+            UseCaseConfig.Builder<VideoCaptureLegacy, VideoCaptureLegacyConfig, Builder>,
+            ImageOutputConfig.Builder<Builder>,
+            ThreadConfig.Builder<Builder> {
+
+        private final MutableOptionsBundle mMutableConfig;
+
+        /** Creates a new Builder object. */
+        public Builder() {
+            this(MutableOptionsBundle.create());
+        }
+
+        private Builder(@NonNull MutableOptionsBundle mutableConfig) {
+            mMutableConfig = mutableConfig;
+
+            Class<?> oldConfigClass =
+                    mutableConfig.retrieveOption(OPTION_TARGET_CLASS, null);
+            if (oldConfigClass != null && !oldConfigClass.equals(VideoCaptureLegacy.class)) {
+                throw new IllegalArgumentException(
+                        "Invalid target class configuration for "
+                                + Builder.this
+                                + ": "
+                                + oldConfigClass);
+            }
+
+            setTargetClass(VideoCaptureLegacy.class);
+        }
+
+        /**
+         * Generates a Builder from another Config object.
+         *
+         * @param configuration An immutable configuration to pre-populate this builder.
+         * @return The new Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        static Builder fromConfig(@NonNull Config configuration) {
+            return new Builder(MutableOptionsBundle.from(configuration));
+        }
+
+
+        /**
+         * Generates a Builder from another Config object
+         *
+         * @param configuration An immutable configuration to pre-populate this builder.
+         * @return The new Builder.
+         */
+        @NonNull
+        public static Builder fromConfig(@NonNull VideoCaptureLegacyConfig configuration) {
+            return new Builder(MutableOptionsBundle.from(configuration));
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public MutableConfig getMutableConfig() {
+            return mMutableConfig;
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public VideoCaptureLegacyConfig getUseCaseConfig() {
+            return new VideoCaptureLegacyConfig(OptionsBundle.from(mMutableConfig));
+        }
+
+        /**
+         * Builds an immutable {@link VideoCaptureLegacyConfig} from the current state.
+         *
+         * @return A {@link VideoCaptureLegacyConfig} populated with the current state.
+         */
+        @Override
+        @NonNull
+        public VideoCaptureLegacy build() {
+            // Error at runtime for using both setTargetResolution and setTargetAspectRatio on
+            // the same config.
+            if (getMutableConfig().retrieveOption(OPTION_TARGET_ASPECT_RATIO, null) != null
+                    && getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION, null) != null) {
+                throw new IllegalArgumentException(
+                        "Cannot use both setTargetResolution and setTargetAspectRatio on the same "
+                                + "config.");
+            }
+            return new VideoCaptureLegacy(getUseCaseConfig());
+        }
+
+        /**
+         * Sets the recording frames per second.
+         *
+         * @param videoFrameRate The requested interval in seconds.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setVideoFrameRate(int videoFrameRate) {
+            getMutableConfig().insertOption(OPTION_VIDEO_FRAME_RATE, videoFrameRate);
+            return this;
+        }
+
+        /**
+         * Sets the encoding bit rate.
+         *
+         * @param bitRate The requested bit rate in bits per second.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setBitRate(int bitRate) {
+            getMutableConfig().insertOption(OPTION_BIT_RATE, bitRate);
+            return this;
+        }
+
+        /**
+         * Sets number of seconds between each key frame in seconds.
+         *
+         * @param interval The requested interval in seconds.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setIFrameInterval(int interval) {
+            getMutableConfig().insertOption(OPTION_INTRA_FRAME_INTERVAL, interval);
+            return this;
+        }
+
+        /**
+         * Sets the bit rate of the audio stream.
+         *
+         * @param bitRate The requested bit rate in bits/s.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setAudioBitRate(int bitRate) {
+            getMutableConfig().insertOption(OPTION_AUDIO_BIT_RATE, bitRate);
+            return this;
+        }
+
+        /**
+         * Sets the sample rate of the audio stream.
+         *
+         * @param sampleRate The requested sample rate in bits/s.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setAudioSampleRate(int sampleRate) {
+            getMutableConfig().insertOption(OPTION_AUDIO_SAMPLE_RATE, sampleRate);
+            return this;
+        }
+
+        /**
+         * Sets the number of audio channels.
+         *
+         * @param channelCount The requested number of audio channels.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setAudioChannelCount(int channelCount) {
+            getMutableConfig().insertOption(OPTION_AUDIO_CHANNEL_COUNT, channelCount);
+            return this;
+        }
+
+        /**
+         * Sets the audio source.
+         *
+         * @param source The audio source. Currently only AudioSource.MIC is supported.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setAudioRecordSource(int source) {
+            getMutableConfig().insertOption(OPTION_AUDIO_RECORD_SOURCE, source);
+            return this;
+        }
+
+        /**
+         * Sets the audio min buffer size.
+         *
+         * @param minBufferSize The requested audio minimum buffer size, in bytes.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setAudioMinBufferSize(int minBufferSize) {
+            getMutableConfig().insertOption(OPTION_AUDIO_MIN_BUFFER_SIZE, minBufferSize);
+            return this;
+        }
+
+        // Implementations of TargetConfig.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setTargetClass(@NonNull Class<VideoCaptureLegacy> targetClass) {
+            getMutableConfig().insertOption(OPTION_TARGET_CLASS, targetClass);
+
+            // If no name is set yet, then generate a unique name
+            if (null == getMutableConfig().retrieveOption(OPTION_TARGET_NAME, null)) {
+                String targetName = targetClass.getCanonicalName() + "-" + UUID.randomUUID();
+                setTargetName(targetName);
+            }
+
+            return this;
+        }
+
+        /**
+         * Sets the name of the target object being configured, used only for debug logging.
+         *
+         * <p>The name should be a value that can uniquely identify an instance of the object being
+         * configured.
+         *
+         * <p>If not set, the target name will default to an unique name automatically generated
+         * with the class canonical name and random UUID.
+         *
+         * @param targetName A unique string identifier for the instance of the class being
+         *                   configured.
+         * @return the current Builder.
+         */
+        @Override
+        @NonNull
+        public Builder setTargetName(@NonNull String targetName) {
+            getMutableConfig().insertOption(OPTION_TARGET_NAME, targetName);
+            return this;
+        }
+
+        // Implementations of ImageOutputConfig.Builder default methods
+
+        /**
+         * Sets the aspect ratio of the intended target for images from this configuration.
+         *
+         * <p>It is not allowed to set both target aspect ratio and target resolution on the same
+         * use case.
+         *
+         * <p>The target aspect ratio is used as a hint when determining the resulting output aspect
+         * ratio which may differ from the request, possibly due to device constraints.
+         * Application code should check the resulting output's resolution.
+         *
+         * <p>If not set, resolutions with aspect ratio 4:3 will be considered in higher
+         * priority.
+         *
+         * @param aspectRatio A {@link AspectRatio} representing the ratio of the
+         *                    target's width and height.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setTargetAspectRatio(@AspectRatio.Ratio int aspectRatio) {
+            getMutableConfig().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
+            return this;
+        }
+
+        /**
+         * Sets the rotation of the intended target for images from this configuration.
+         *
+         * <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link
+         * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
+         * Rotation values are relative to the "natural" rotation, {@link Surface#ROTATION_0}.
+         *
+         * <p>If not set, the target rotation will default to the value of
+         * {@link Display#getRotation()} of the default display at the time the use case is
+         * created. The use case is fully created once it has been attached to a camera.
+         *
+         * @param rotation The rotation of the intended target.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setTargetRotation(@RotationValue int rotation) {
+            getMutableConfig().insertOption(OPTION_TARGET_ROTATION, rotation);
+            return this;
+        }
+
+        /**
+         * Sets the resolution of the intended target from this configuration.
+         *
+         * <p>The target resolution attempts to establish a minimum bound for the image resolution.
+         * The actual image resolution will be the closest available resolution in size that is not
+         * smaller than the target resolution, as determined by the Camera implementation. However,
+         * if no resolution exists that is equal to or larger than the target resolution, the
+         * nearest available resolution smaller than the target resolution will be chosen.
+         *
+         * <p>It is not allowed to set both target aspect ratio and target resolution on the same
+         * use case.
+         *
+         * <p>The target aspect ratio will also be set the same as the aspect ratio of the provided
+         * {@link Size}. Make sure to set the target resolution with the correct orientation.
+         *
+         * @param resolution The target resolution to choose from supported output sizes list.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setTargetResolution(@NonNull Size resolution) {
+            getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION, resolution);
+            return this;
+        }
+
+        /**
+         * Sets the default resolution of the intended target from this configuration.
+         *
+         * @param resolution The default resolution to choose from supported output sizes list.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setDefaultResolution(@NonNull Size resolution) {
+            getMutableConfig().insertOption(OPTION_DEFAULT_RESOLUTION, resolution);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setMaxResolution(@NonNull Size resolution) {
+            getMutableConfig().insertOption(OPTION_MAX_RESOLUTION, resolution);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setSupportedResolutions(@NonNull List<Pair<Integer, Size[]>> resolutions) {
+            getMutableConfig().insertOption(OPTION_SUPPORTED_RESOLUTIONS, resolutions);
+            return this;
+        }
+
+        // Implementations of ThreadConfig.Builder default methods
+
+        /**
+         * Sets the default executor that will be used for background tasks.
+         *
+         * <p>If not set, the background executor will default to an automatically generated
+         * {@link Executor}.
+         *
+         * @param executor The executor which will be used for background tasks.
+         * @return the current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setBackgroundExecutor(@NonNull Executor executor) {
+            getMutableConfig().insertOption(OPTION_BACKGROUND_EXECUTOR, executor);
+            return this;
+        }
+
+        // Implementations of UseCaseConfig.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setDefaultSessionConfig(@NonNull SessionConfig sessionConfig) {
+            getMutableConfig().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setDefaultCaptureConfig(@NonNull CaptureConfig captureConfig) {
+            getMutableConfig().insertOption(OPTION_DEFAULT_CAPTURE_CONFIG, captureConfig);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setSessionOptionUnpacker(
+                @NonNull SessionConfig.OptionUnpacker optionUnpacker) {
+            getMutableConfig().insertOption(OPTION_SESSION_CONFIG_UNPACKER, optionUnpacker);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setCaptureOptionUnpacker(
+                @NonNull CaptureConfig.OptionUnpacker optionUnpacker) {
+            getMutableConfig().insertOption(OPTION_CAPTURE_CONFIG_UNPACKER, optionUnpacker);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setSurfaceOccupancyPriority(int priority) {
+            getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY)
+        @Override
+        @NonNull
+        public Builder setCameraSelector(@NonNull CameraSelector cameraSelector) {
+            getMutableConfig().insertOption(OPTION_CAMERA_SELECTOR, cameraSelector);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setUseCaseEventCallback(
+                @NonNull EventCallback useCaseEventCallback) {
+            getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_CALLBACK, useCaseEventCallback);
+            return this;
+        }
+    }
+
+    /**
+     * Info about the saved video file.
+     */
+    public static class OutputFileResults {
+        @Nullable
+        private Uri mSavedUri;
+
+        OutputFileResults(@Nullable Uri savedUri) {
+            mSavedUri = savedUri;
+        }
+
+        /**
+         * Returns the {@link Uri} of the saved video file.
+         *
+         * <p> This field is only returned if the {@link OutputFileOptions} is
+         * backed by {@link MediaStore} constructed with
+         * {@link OutputFileOptions}.
+         */
+        @Nullable
+        public Uri getSavedUri() {
+            return mSavedUri;
+        }
+    }
+
+    /**
+     * Options for saving newly captured video.
+     *
+     * <p> this class is used to configure save location and metadata. Save location can be
+     * either a {@link File}, {@link MediaStore}. The metadata will be
+     * stored with the saved video.
+     */
+    public static final class OutputFileOptions {
+
+        // Empty metadata object used as a placeholder for no user-supplied metadata.
+        // Should be initialized to all default values.
+        private static final Metadata EMPTY_METADATA = new Metadata();
+
+        @Nullable
+        private final File mFile;
+        @Nullable
+        private final FileDescriptor mFileDescriptor;
+        @Nullable
+        private final ContentResolver mContentResolver;
+        @Nullable
+        private final Uri mSaveCollection;
+        @Nullable
+        private final ContentValues mContentValues;
+        @Nullable
+        private final Metadata mMetadata;
+
+        OutputFileOptions(@Nullable File file,
+                @Nullable FileDescriptor fileDescriptor,
+                @Nullable ContentResolver contentResolver,
+                @Nullable Uri saveCollection,
+                @Nullable ContentValues contentValues,
+                @Nullable Metadata metadata) {
+            mFile = file;
+            mFileDescriptor = fileDescriptor;
+            mContentResolver = contentResolver;
+            mSaveCollection = saveCollection;
+            mContentValues = contentValues;
+            mMetadata = metadata == null ? EMPTY_METADATA : metadata;
+        }
+
+        /** Returns the File object which is set by the {@link Builder}. */
+        @Nullable
+        File getFile() {
+            return mFile;
+        }
+
+        /**
+         * Returns the FileDescriptor object which is set by the {@link Builder}.
+         */
+        @Nullable
+        FileDescriptor getFileDescriptor() {
+            return mFileDescriptor;
+        }
+
+        /** Returns the content resolver which is set by the {@link Builder}. */
+        @Nullable
+        ContentResolver getContentResolver() {
+            return mContentResolver;
+        }
+
+        /** Returns the URI which is set by the {@link Builder}. */
+        @Nullable
+        Uri getSaveCollection() {
+            return mSaveCollection;
+        }
+
+        /** Returns the content values which is set by the {@link Builder}. */
+        @Nullable
+        ContentValues getContentValues() {
+            return mContentValues;
+        }
+
+        /** Return the metadata which is set by the {@link Builder}.. */
+        @Nullable
+        Metadata getMetadata() {
+            return mMetadata;
+        }
+
+        /** Checking the caller wants to save video to MediaStore. */
+        boolean isSavingToMediaStore() {
+            return getSaveCollection() != null && getContentResolver() != null
+                    && getContentValues() != null;
+        }
+
+        /** Checking the caller wants to save video to a File. */
+        boolean isSavingToFile() {
+            return getFile() != null;
+        }
+
+        /** Checking the caller wants to save video to a FileDescriptor. */
+        boolean isSavingToFileDescriptor() {
+            return getFileDescriptor() != null;
+        }
+
+        /**
+         * Builder class for {@link OutputFileOptions}.
+         */
+        public static final class Builder {
+            @Nullable
+            private File mFile;
+            @Nullable
+            private FileDescriptor mFileDescriptor;
+            @Nullable
+            private ContentResolver mContentResolver;
+            @Nullable
+            private Uri mSaveCollection;
+            @Nullable
+            private ContentValues mContentValues;
+            @Nullable
+            private Metadata mMetadata;
+
+            /**
+             * Creates options to write captured video to a {@link File}.
+             *
+             * @param file save location of the video.
+             */
+            public Builder(@NonNull File file) {
+                mFile = file;
+            }
+
+            /**
+             * Creates options to write captured video to a {@link FileDescriptor}.
+             *
+             * <p>Using a FileDescriptor to record a video is only supported for Android 8.0 or
+             * above.
+             *
+             * @param fileDescriptor to save the video.
+             * @throws IllegalArgumentException when the device is not running Android 8.0 or above.
+             */
+            public Builder(@NonNull FileDescriptor fileDescriptor) {
+                Preconditions.checkArgument(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O,
+                        "Using a FileDescriptor to record a video is only supported for Android 8"
+                                + ".0 or above.");
+
+                mFileDescriptor = fileDescriptor;
+            }
+
+            /**
+             * Creates options to write captured video to {@link MediaStore}.
+             *
+             * Example:
+             *
+             * <pre>{@code
+             *
+             * ContentValues contentValues = new ContentValues();
+             * contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "NEW_VIDEO");
+             * contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
+             *
+             * OutputFileOptions options = new OutputFileOptions.Builder(
+             *         getContentResolver(),
+             *         MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+             *         contentValues).build();
+             *
+             * }</pre>
+             *
+             * @param contentResolver to access {@link MediaStore}
+             * @param saveCollection  The URL of the table to insert into.
+             * @param contentValues   to be included in the created video file.
+             */
+            public Builder(@NonNull ContentResolver contentResolver,
+                    @NonNull Uri saveCollection,
+                    @NonNull ContentValues contentValues) {
+                mContentResolver = contentResolver;
+                mSaveCollection = saveCollection;
+                mContentValues = contentValues;
+            }
+
+            /**
+             * Sets the metadata to be stored with the saved video.
+             *
+             * @param metadata Metadata to be stored with the saved video.
+             */
+            @NonNull
+            public Builder setMetadata(@NonNull Metadata metadata) {
+                mMetadata = metadata;
+                return this;
+            }
+
+            /**
+             * Builds {@link OutputFileOptions}.
+             */
+            @NonNull
+            public OutputFileOptions build() {
+                return new OutputFileOptions(mFile, mFileDescriptor, mContentResolver,
+                        mSaveCollection, mContentValues, mMetadata);
+            }
+        }
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureLegacyConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureLegacyConfig.java
new file mode 100644
index 0000000..9f67287
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureLegacyConfig.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2019 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.impl;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.ImageFormatConstants;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.OptionsBundle;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.internal.ThreadConfig;
+import androidx.camera.video.VideoCaptureLegacy;
+
+/**
+ * Config for a video capture use case.
+ *
+ * <p>In the earlier stage, the VideoCaptureLegacy is deprioritized.
+ */
+public final class VideoCaptureLegacyConfig
+        implements UseCaseConfig<VideoCaptureLegacy>,
+        ImageOutputConfig,
+        ThreadConfig {
+
+    // Option Declarations:
+    // *********************************************************************************************
+
+    public static final Option<Integer> OPTION_VIDEO_FRAME_RATE =
+            Option.create("camerax.video.videoCaptureLegacy.recordingFrameRate", int.class);
+    public static final Option<Integer> OPTION_BIT_RATE =
+            Option.create("camerax.video.videoCaptureLegacy.bitRate", int.class);
+    public static final Option<Integer> OPTION_INTRA_FRAME_INTERVAL =
+            Option.create("camerax.video.videoCaptureLegacy.intraFrameInterval", int.class);
+    public static final Option<Integer> OPTION_AUDIO_BIT_RATE =
+            Option.create("camerax.video.videoCaptureLegacy.audioBitRate", int.class);
+    public static final Option<Integer> OPTION_AUDIO_SAMPLE_RATE =
+            Option.create("camerax.video.videoCaptureLegacy.audioSampleRate", int.class);
+    public static final Option<Integer> OPTION_AUDIO_CHANNEL_COUNT =
+            Option.create("camerax.video.videoCaptureLegacy.audioChannelCount", int.class);
+    public static final Option<Integer> OPTION_AUDIO_RECORD_SOURCE =
+            Option.create("camerax.video.videoCaptureLegacy.audioRecordSource", int.class);
+    public static final Option<Integer> OPTION_AUDIO_MIN_BUFFER_SIZE =
+            Option.create("camerax.video.videoCaptureLegacy.audioMinBufferSize", int.class);
+
+    // *********************************************************************************************
+
+    private final OptionsBundle mConfig;
+
+    public VideoCaptureLegacyConfig(@NonNull OptionsBundle config) {
+        mConfig = config;
+    }
+
+    /**
+     * Returns the recording frames per second.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getVideoFrameRate(int valueIfMissing) {
+        return retrieveOption(OPTION_VIDEO_FRAME_RATE, valueIfMissing);
+    }
+
+    /**
+     * Returns the recording frames per second.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getVideoFrameRate() {
+        return retrieveOption(OPTION_VIDEO_FRAME_RATE);
+    }
+
+    /**
+     * Returns the encoding bit rate.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getBitRate(int valueIfMissing) {
+        return retrieveOption(OPTION_BIT_RATE, valueIfMissing);
+    }
+
+    /**
+     * Returns the encoding bit rate.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getBitRate() {
+        return retrieveOption(OPTION_BIT_RATE);
+    }
+
+    /**
+     * Returns the number of seconds between each key frame.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getIFrameInterval(int valueIfMissing) {
+        return retrieveOption(OPTION_INTRA_FRAME_INTERVAL, valueIfMissing);
+    }
+
+    /**
+     * Returns the number of seconds between each key frame.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getIFrameInterval() {
+        return retrieveOption(OPTION_INTRA_FRAME_INTERVAL);
+    }
+
+    /**
+     * Returns the audio encoding bit rate.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getAudioBitRate(int valueIfMissing) {
+        return retrieveOption(OPTION_AUDIO_BIT_RATE, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio encoding bit rate.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getAudioBitRate() {
+        return retrieveOption(OPTION_AUDIO_BIT_RATE);
+    }
+
+    /**
+     * Returns the audio sample rate.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getAudioSampleRate(int valueIfMissing) {
+        return retrieveOption(OPTION_AUDIO_SAMPLE_RATE, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio sample rate.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getAudioSampleRate() {
+        return retrieveOption(OPTION_AUDIO_SAMPLE_RATE);
+    }
+
+    /**
+     * Returns the audio channel count.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getAudioChannelCount(int valueIfMissing) {
+        return retrieveOption(OPTION_AUDIO_CHANNEL_COUNT, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio channel count.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getAudioChannelCount() {
+        return retrieveOption(OPTION_AUDIO_CHANNEL_COUNT);
+    }
+
+    /**
+     * Returns the audio recording source.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getAudioRecordSource(int valueIfMissing) {
+        return retrieveOption(OPTION_AUDIO_RECORD_SOURCE, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio recording source.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getAudioRecordSource() {
+        return retrieveOption(OPTION_AUDIO_RECORD_SOURCE);
+    }
+
+    /**
+     * Returns the audio minimum buffer size, in bytes.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getAudioMinBufferSize(int valueIfMissing) {
+        return retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio minimum buffer size, in bytes.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getAudioMinBufferSize() {
+        return retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE);
+    }
+
+    /**
+     * Retrieves the format of the image that is fed as input.
+     *
+     * <p>This should always be PRIVATE for VideoCaptureLegacy.
+     */
+    @Override
+    public int getInputFormat() {
+        return ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+    }
+
+    @NonNull
+    @Override
+    public Config getConfig() {
+        return mConfig;
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java b/camera/camera-video/src/main/java/androidx/camera/video/impl/package-info.java
similarity index 94%
rename from camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java
rename to camera/camera-video/src/main/java/androidx/camera/video/impl/package-info.java
index d6b6436..1e6190c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/impl/package-info.java
@@ -18,6 +18,6 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-package androidx.camera.core.impl.quirk;
+package androidx.camera.video.impl;
 
 import androidx.annotation.RestrictTo;
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/AudioEncoderConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/AudioEncoderConfig.java
new file mode 100644
index 0000000..157d525
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/AudioEncoderConfig.java
@@ -0,0 +1,98 @@
+/*
+ * 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.media.MediaFormat;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+/** {@inheritDoc} */
+@AutoValue
+public abstract class AudioEncoderConfig implements EncoderConfig {
+
+    // Restrict constructor to same package
+    AudioEncoderConfig() {
+    }
+
+    /** Returns a build for this config. */
+    @NonNull
+    public static Builder builder() {
+        return new AutoValue_AudioEncoderConfig.Builder();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @NonNull
+    public abstract String getMimeType();
+
+    /** Gets the bitrate. */
+    public abstract int getBitrate();
+
+    /** Gets the sample bitrate. */
+    public abstract int getSampleRate();
+
+    /** Gets the channel mask. */
+    public abstract int getChannelMask();
+
+    /** Gets the channel count. */
+    public abstract int getChannelCount();
+
+    /** {@inheritDoc} */
+    @NonNull
+    @Override
+    public MediaFormat toMediaFormat() {
+        MediaFormat mediaFormat = MediaFormat.createAudioFormat(getMimeType(), getSampleRate(),
+                getChannelCount());
+        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, getBitrate());
+        mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, getChannelMask());
+        return mediaFormat;
+    }
+
+    /** The builder of the config. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+        // Restrict construction to same package
+        Builder() {
+        }
+
+        /** Sets the mime type. */
+        @NonNull
+        public abstract Builder setMimeType(@NonNull String mimeType);
+
+        /** Sets the bitrate. */
+        @NonNull
+        public abstract Builder setBitrate(int bitrate);
+
+        /** Sets the sample rate. */
+        @NonNull
+        public abstract Builder setSampleRate(int sampleRate);
+
+        /** Sets the channel mask. */
+        @NonNull
+        public abstract Builder setChannelMask(int channelMask);
+
+        /** Sets the channel count. */
+        @NonNull
+        public abstract Builder setChannelCount(int channelCount);
+
+        /** Builds the config instance. */
+        @NonNull
+        public abstract AudioEncoderConfig build();
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodeException.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodeException.java
new file mode 100644
index 0000000..4183749
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodeException.java
@@ -0,0 +1,55 @@
+/*
+ * 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 androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** An exception thrown to indicate an error has occurred during encoding. */
+public class EncodeException extends Exception {
+    /** Unknown error. */
+    public static final int ERROR_UNKNOWN = 0;
+
+    /** Error occurred during encoding. */
+    public static final int ERROR_CODEC = 1;
+
+    /** Describes the error that occurred during encoding. */
+    @IntDef({ERROR_UNKNOWN, ERROR_CODEC})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ErrorType {}
+
+    @ErrorType
+    private final int mErrorType;
+
+    public EncodeException(@ErrorType int errorType, @Nullable String message,
+            @Nullable Throwable cause) {
+        super(message, cause);
+        mErrorType = errorType;
+    }
+
+    /**
+     * Returns the encode error type, can have one of the following values:
+     * {@link #ERROR_UNKNOWN}, {@link #ERROR_CODEC}
+     */
+    @ErrorType
+    public int getErrorType() {
+        return mErrorType;
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedData.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedData.java
new file mode 100644
index 0000000..34cdf5c
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedData.java
@@ -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.
+ */
+
+package androidx.camera.video.internal.encoder;
+
+import android.media.MediaCodec;
+
+import androidx.annotation.NonNull;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.nio.ByteBuffer;
+
+/**
+ * The encoded data which is generated by the {@link Encoder}.
+ *
+ * <p>Once {@link EncodedData} is no longer needed, it has to call {@link #close} to return to
+ * encoder, otherwise it may cause leakage or failure.
+ *
+ * @see EncoderCallback#onEncodedData
+ */
+public interface EncodedData extends AutoCloseable {
+    /**
+     * Gets the {@link ByteBuffer} of the encoded data.
+     *
+     * <p>After {@link #close} is called, the byte buffer will be returned to encoder. Therefore,
+     * make sure not to use this byte buffer after {@link #close} is called, otherwise it may get
+     * uncertain behavior.
+     */
+    @NonNull
+    ByteBuffer getByteBuffer();
+
+    /**
+     * Gets the {@link ByteBuffer}'s additional information.
+     *
+     * @see MediaCodec.BufferInfo
+     */
+    @NonNull
+    MediaCodec.BufferInfo getBufferInfo();
+
+    /** Gets the timestamp of the encoded data in microseconds. */
+    long getPresentationTimeUs();
+
+    /** The encoded data should be explicitly closed in order to release the resources. */
+    @Override
+    void close();
+
+    /** The {@link ListenableFuture} that is complete when {@link #close} is called. */
+    @NonNull
+    ListenableFuture<Void> getClosedFuture();
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedDataImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedDataImpl.java
new file mode 100644
index 0000000..723edce
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedDataImpl.java
@@ -0,0 +1,110 @@
+/*
+ * 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.media.MediaCodec;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** {@inheritDoc} */
+public class EncodedDataImpl implements EncodedData {
+    private final MediaCodec mMediaCodec;
+    private final MediaCodec.BufferInfo mBufferInfo;
+    private final int mBufferIndex;
+
+    private final ByteBuffer mByteBuffer;
+    private final ListenableFuture<Void> mClosedFuture;
+    private final CallbackToFutureAdapter.Completer<Void> mClosedCompleter;
+    private final AtomicBoolean mClosed = new AtomicBoolean(false);
+
+    EncodedDataImpl(@NonNull MediaCodec mediaCodec, int bufferIndex,
+            @NonNull MediaCodec.BufferInfo bufferInfo) throws MediaCodec.CodecException {
+        mMediaCodec = Preconditions.checkNotNull(mediaCodec);
+        mBufferIndex = bufferIndex;
+        mByteBuffer = mediaCodec.getOutputBuffer(bufferIndex);
+        mBufferInfo = Preconditions.checkNotNull(bufferInfo);
+        AtomicReference<CallbackToFutureAdapter.Completer<Void>> ref = new AtomicReference<>();
+        mClosedFuture = CallbackToFutureAdapter.getFuture(
+                completer -> {
+                    ref.set(completer);
+                    return "Data closed";
+                });
+        mClosedCompleter = Preconditions.checkNotNull(ref.get());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @NonNull
+    public ByteBuffer getByteBuffer() {
+        throwIfClosed();
+        mByteBuffer.position(mBufferInfo.offset);
+        mByteBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
+        return mByteBuffer;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @NonNull
+    public MediaCodec.BufferInfo getBufferInfo() {
+        throwIfClosed();
+        return mBufferInfo;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public long getPresentationTimeUs() {
+        throwIfClosed();
+        return getBufferInfo().presentationTimeUs;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void close() {
+        if (mClosed.getAndSet(true)) {
+            return;
+        }
+        try {
+            mMediaCodec.releaseOutputBuffer(mBufferIndex, false);
+        } catch (MediaCodec.CodecException e) {
+            mClosedCompleter.setException(e);
+            return;
+        }
+        mClosedCompleter.set(null);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @NonNull
+    public ListenableFuture<Void> getClosedFuture() {
+        return Futures.nonCancellationPropagating(mClosedFuture);
+    }
+
+    private void throwIfClosed() {
+        if (mClosed.get()) {
+            throw new IllegalStateException("encoded data is closed.");
+        }
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java
new file mode 100644
index 0000000..da6ebce
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java
@@ -0,0 +1,125 @@
+/*
+ * 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.view.Surface;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.Executor;
+
+/**
+ * The encoder interface.
+ *
+ * <p>An encoder could be either a video encoder or an audio encoder. The interface defines the
+ * common APIs to communicate with an encoder.
+ */
+public interface Encoder {
+
+    /** Returns the encoder's input instance. */
+    @NonNull
+    EncoderInput getInput();
+
+    /**
+     * Starts the encoder.
+     *
+     * <p>If the encoder is not started yet, it will first trigger
+     * {@link EncoderCallback#onEncodeStart}. Then continually invoke the
+     * {@link EncoderCallback#onEncodedData} callback until the encoder is paused, stopped or
+     * released. It can call {@link #pause} to pause the encoding after started. If the encoder is
+     * in paused state, then calling this method will resume the encoding.
+     */
+    void start();
+
+    /**
+     * Stops the encoder.
+     *
+     * <p>It will trigger {@link EncoderCallback#onEncodeStop} after the last encoded data. It can
+     * call {@link #start} to start again.
+     */
+    void stop();
+
+    /**
+     * Pauses the encoder.
+     *
+     * <p>{@link #pause} only work between {@link #start} and {@link #stop}.
+     * Once the encoder is paused, it will drop the input data until {@link #start} is invoked
+     * again.
+     */
+    void pause();
+
+    /**
+     * Releases the encoder.
+     *
+     * <p>Once the encoder is released, it cannot be used anymore. Any other method call after
+     * the encoder is released will get {@link IllegalStateException}.
+     */
+    void release();
+
+    /**
+     * Sets callback to encoder.
+     *
+     * @param encoderCallback the encoder callback
+     * @param executor the callback executor
+     */
+    void setEncoderCallback(@NonNull EncoderCallback encoderCallback, @NonNull Executor executor);
+
+    /** The encoder's input. */
+    interface EncoderInput {
+    }
+
+    /**
+     * A SurfaceInput provides a {@link Surface} as the interface to receive video raw data.
+     *
+     * <p>SurfaceInput is only available for video encoder. It has to set
+     * {@link #setOnSurfaceUpdateListener} to obtain the {@link Surface} update. It is the caller's
+     * responsibility to release the updated {@link Surface}.
+     */
+    interface SurfaceInput extends EncoderInput {
+
+        void setOnSurfaceUpdateListener(@NonNull Executor executor,
+                @NonNull OnSurfaceUpdateListener listener);
+
+        /**
+         * An interface for receiving the update event of the input {@link Surface} of the encoder.
+         */
+        interface OnSurfaceUpdateListener {
+            /**
+             * Notifies the surface is updated.
+             *
+             * @param surface the updated surface
+             */
+            void onSurfaceUpdate(@NonNull Surface surface);
+        }
+    }
+
+    /** A ByteBufferInput provides {@link #putByteBuffer} method to send raw data. */
+    interface ByteBufferInput extends EncoderInput {
+
+        /**
+         * Puts an input raw {@link ByteBuffer} to the encoder.
+         *
+         * <p>The input {@code ByteBuffer} must be put when encoder is in started and not paused
+         * state, otherwise the {@code ByteBuffer} will be dropped directly. Then the encoded data
+         * will be sent via {@link EncoderCallback#onEncodedData} callback.
+         *
+         * @param byteBuffer the input byte buffer
+         */
+        void putByteBuffer(@NonNull ByteBuffer byteBuffer);
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderCallback.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderCallback.java
new file mode 100644
index 0000000..44dd8b3
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderCallback.java
@@ -0,0 +1,82 @@
+/*
+ * 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 androidx.annotation.NonNull;
+
+/**
+ * The encoder callback event.
+ */
+public interface EncoderCallback {
+
+    /** The method called before the first encoded data. */
+    void onEncodeStart();
+
+    /** The method called after the last encoded data. */
+    void onEncodeStop();
+
+    /**
+     * The method called when error occurs while encoding.
+     *
+     * <p>After that, {@link #onEncodedData} and {@link #onEncodeStop} will not be triggered.
+     *
+     * @param e the encode error
+     */
+    void onEncodeError(@NonNull EncodeException e);
+
+    /**
+     * The method called when a new encoded data comes.
+     *
+     * @param encodedData the encoded data
+     */
+    void onEncodedData(@NonNull EncodedData encodedData);
+
+    /**
+     * The method called when encoder gets a new output config.
+     *
+     * @param outputConfig the output config
+     */
+    void onOutputConfigUpdate(@NonNull OutputConfig outputConfig);
+
+    /** An empty implementation. */
+    EncoderCallback EMPTY = new EncoderCallback() {
+        /** {@inheritDoc} */
+        @Override
+        public void onEncodeStart() {
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void onEncodeStop() {
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void onEncodeError(@NonNull EncodeException e) {
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void onEncodedData(@NonNull EncodedData encodedData) {
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void onOutputConfigUpdate(@NonNull OutputConfig outputConfig) {
+        }
+    };
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java
new file mode 100644
index 0000000..9e69832
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java
@@ -0,0 +1,46 @@
+/*
+ * 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.media.MediaFormat;
+
+import androidx.annotation.NonNull;
+
+/**
+ * The configuration represents the required parameters to configure an encoder.
+ *
+ * <p>An {@code EncoderConfig} is used to configure an {@link Encoder}.
+ */
+public interface EncoderConfig {
+    /**
+     * The mime type of the encoder.
+     *
+     * <p>For example, "video/avc" for a video encoder and "audio/mp4a-latm" for an audio encoder.
+     *
+     * @See {@link MediaFormat}
+     */
+    @NonNull
+    String getMimeType();
+
+    /**
+     * Transfers the config to a {@link MediaFormat}.
+     *
+     * @return the result {@link MediaFormat}
+     */
+    @NonNull
+    MediaFormat toMediaFormat() throws InvalidConfigException;
+}
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
new file mode 100644
index 0000000..00280df
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -0,0 +1,846 @@
+/*
+ * 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 static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.CONFIGURED;
+import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.PAUSED;
+import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.PENDING_RELEASE;
+import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.PENDING_START;
+import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.PENDING_START_PAUSED;
+import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.RELEASED;
+import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.STARTED;
+import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.STOPPING;
+
+import android.annotation.SuppressLint;
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.Logger;
+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.core.util.Consumer;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * The encoder implementation.
+ *
+ * <p>An encoder could be either a video encoder or an audio encoder.
+ */
+public class EncoderImpl implements Encoder {
+    private static final String TAG = "Encoder";
+
+    enum InternalState {
+        /**
+         * The initial state.
+         */
+        CONFIGURED,
+
+        /**
+         * The state is when encoder is in {@link InternalState#CONFIGURED} state and {@link #start}
+         * is called.
+         */
+        STARTED,
+
+        /**
+         * The state is when encoder is in {@link InternalState#STARTED} state and {@link #pause}
+         * is called.
+         */
+        PAUSED,
+
+        /**
+         * The state is when encoder is in {@link InternalState#STARTED} state and {@link #stop} is
+         * called.
+         */
+        STOPPING,
+
+        /**
+         * The state is when the encoder is in {@link InternalState#STOPPING} state and a
+         * {@link #start} is called. It is an extension of {@link InternalState#STOPPING}.
+         */
+        PENDING_START,
+
+        /**
+         * The state is when the encoder is in {@link InternalState#STOPPING} state, then
+         * {@link #start} and {@link #pause} is called. It is an extension of
+         * {@link InternalState#STOPPING}.
+         */
+        PENDING_START_PAUSED,
+
+        /**
+         * The state is when the encoder is in {@link InternalState#STOPPING} state and a
+         * {@link #release} is called. It is an extension of {@link InternalState#STOPPING}.
+         */
+        PENDING_RELEASE,
+
+        /** The state is when the encoder is released. */
+        RELEASED,
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    final Object mLock = new Object();
+    private final MediaFormat mMediaFormat;
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @GuardedBy("mLock")
+    final MediaCodec mMediaCodec;
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    final EncoderInput mEncoderInput;
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    final Executor mExecutor;
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @GuardedBy("mLock")
+    EncoderCallback mEncoderCallback = EncoderCallback.EMPTY;
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @GuardedBy("mLock")
+    Executor mEncoderCallbackExecutor = CameraXExecutors.mainThreadExecutor();
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @GuardedBy("mLock")
+    InternalState mState;
+
+    /**
+     * Creates the encoder with a {@link EncoderConfig}
+     *
+     * @param executor the executor suitable for background task
+     * @param encoderConfig the encoder config
+     * @throws InvalidConfigException when the encoder cannot be configured.
+     */
+    public EncoderImpl(@NonNull Executor executor, @NonNull EncoderConfig encoderConfig)
+            throws InvalidConfigException {
+        Preconditions.checkNotNull(executor);
+        Preconditions.checkNotNull(encoderConfig);
+
+        mExecutor = CameraXExecutors.newSequentialExecutor(executor);
+
+        if (encoderConfig instanceof AudioEncoderConfig) {
+            mEncoderInput = new ByteBufferInput();
+        } else if (encoderConfig instanceof VideoEncoderConfig) {
+            mEncoderInput = new SurfaceInput();
+        } else {
+            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();
+
+        try {
+            reset();
+        } catch (MediaCodec.CodecException e) {
+            throw new InvalidConfigException(e);
+        }
+
+        setState(CONFIGURED);
+    }
+
+    @SuppressWarnings("GuardedBy")
+    // It complains SurfaceInput#resetSurface and ByteBufferInput#clearFreeBuffers don't hold mLock
+    @GuardedBy("mLock")
+    private void reset() {
+        mMediaCodec.reset();
+        mMediaCodec.setCallback(new MediaCodecCallback());
+        mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+
+        if (mEncoderInput instanceof SurfaceInput) {
+            ((SurfaceInput) mEncoderInput).resetSurface();
+        } else if (mEncoderInput instanceof ByteBufferInput) {
+            ((ByteBufferInput) mEncoderInput).clearFreeBuffers();
+        }
+    }
+
+    /** Gets the {@link EncoderInput} of the encoder */
+    @Override
+    @NonNull
+    public EncoderInput getInput() {
+        return mEncoderInput;
+    }
+
+    /**
+     * Starts the encoder.
+     *
+     * <p>If the encoder is not started yet, it will first trigger
+     * {@link EncoderCallback#onEncodeStart}. Then continually invoke the
+     * {@link EncoderCallback#onEncodedData} callback until the encoder is paused, stopped or
+     * released. It can call {@link #pause} to pause the encoding after started. If the encoder is
+     * in paused state, then calling this method will resume the encoding.
+     */
+    @Override
+    public void start() {
+        synchronized (mLock) {
+            switch (mState) {
+                case CONFIGURED:
+                    try {
+                        mMediaCodec.start();
+                    } catch (MediaCodec.CodecException e) {
+                        handleEncodeError(e);
+                        return;
+                    }
+                    setState(STARTED);
+                    break;
+                case PAUSED:
+                    if (mEncoderInput instanceof SurfaceInput) {
+                        updatePauseToMediaCodec(false);
+                    }
+                    setState(STARTED);
+                    break;
+                case STARTED:
+                case PENDING_START:
+                    // Do nothing
+                    break;
+                case STOPPING:
+                case PENDING_START_PAUSED:
+                    setState(PENDING_START);
+                    break;
+                case PENDING_RELEASE:
+                case RELEASED:
+                    throw new IllegalStateException("Encoder is released");
+                default:
+                    throw new IllegalStateException("Unknown state: " + mState);
+            }
+        }
+    }
+
+    /**
+     * Stops the encoder.
+     *
+     * <p>It will trigger {@link EncoderCallback#onEncodeStop} after the last encoded data. It can
+     * call {@link #start} to start again.
+     */
+    @SuppressWarnings("GuardedBy")
+    // It complains ByteBufferInput#signalEndOfInputStream doesn't hold mLock
+    @Override
+    public void stop() {
+        synchronized (mLock) {
+            switch (mState) {
+                case CONFIGURED:
+                case STOPPING:
+                    // Do nothing
+                    break;
+                case STARTED:
+                case PAUSED:
+                    setState(STOPPING);
+                    if (mEncoderInput instanceof ByteBufferInput) {
+                        ((ByteBufferInput) mEncoderInput).signalEndOfInputStream();
+                    } else if (mEncoderInput instanceof SurfaceInput) {
+                        try {
+                            mMediaCodec.signalEndOfInputStream();
+                        } catch (MediaCodec.CodecException e) {
+                            handleEncodeError(e);
+                        }
+                    }
+                    break;
+                case PENDING_START:
+                case PENDING_START_PAUSED:
+                    setState(CONFIGURED);
+                    break;
+                case PENDING_RELEASE:
+                case RELEASED:
+                    throw new IllegalStateException("Encoder is released");
+                default:
+                    throw new IllegalStateException("Unknown state: " + mState);
+            }
+        }
+    }
+
+    /**
+     * Pauses the encoder.
+     *
+     * <p>{@link #pause} only work between {@link #start} and {@link #stop}. Once the encoder is
+     * paused, it will drop the input data until {@link #start} is invoked again.
+     */
+    @Override
+    public void pause() {
+        synchronized (mLock) {
+            switch (mState) {
+                case CONFIGURED:
+                case PAUSED:
+                case STOPPING:
+                case PENDING_START_PAUSED:
+                    // Do nothing
+                    break;
+                case PENDING_START:
+                    setState(PENDING_START_PAUSED);
+                    break;
+                case STARTED:
+                    if (mEncoderInput instanceof SurfaceInput) {
+                        updatePauseToMediaCodec(true);
+                    }
+                    setState(PAUSED);
+                    break;
+                case PENDING_RELEASE:
+                case RELEASED:
+                    throw new IllegalStateException("Encoder is released");
+                default:
+                    throw new IllegalStateException("Unknown state: " + mState);
+            }
+        }
+    }
+
+    /**
+     * Releases the encoder.
+     *
+     * <p>Once the encoder is released, it cannot be used anymore. Any other method call after
+     * the encoder is released will get {@link IllegalStateException}. If it is in encoding, make
+     * sure call {@link #stop} before {@link #release} to normally end the stream, or it may get
+     * uncertain result if call {@link #release} while encoding.
+     */
+    @Override
+    public void release() {
+        synchronized (mLock) {
+            switch (mState) {
+                case CONFIGURED:
+                case STARTED:
+                case PAUSED:
+                    mMediaCodec.release();
+                    setState(RELEASED);
+                    break;
+                case STOPPING:
+                case PENDING_START:
+                case PENDING_START_PAUSED:
+                    setState(PENDING_RELEASE);
+                    break;
+                case PENDING_RELEASE:
+                case RELEASED:
+                    // Do nothing
+                    break;
+                default:
+                    throw new IllegalStateException("Unknown state: " + mState);
+            }
+        }
+    }
+
+    /**
+     * Sets callback to encoder.
+     *
+     * @param encoderCallback the encoder callback
+     * @param executor the callback executor
+     */
+    @Override
+    public void setEncoderCallback(
+            @NonNull EncoderCallback encoderCallback,
+            @NonNull Executor executor) {
+        synchronized (mLock) {
+            mEncoderCallback = encoderCallback;
+            mEncoderCallbackExecutor = executor;
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void setState(InternalState state) {
+        Logger.d(TAG, "Transitioning encoder internal state: " + mState + " --> " + state);
+        mState = state;
+    }
+
+    @GuardedBy("mLock")
+    private void updatePauseToMediaCodec(boolean paused) {
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(MediaCodec.PARAMETER_KEY_SUSPEND, paused);
+        mMediaCodec.setParameters(bundle);
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @GuardedBy("mLock")
+    void handleEncodeError(@NonNull MediaCodec.CodecException e) {
+        handleEncodeError(EncodeException.ERROR_CODEC, e.getMessage(), e);
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @GuardedBy("mLock")
+    void handleEncodeError(@EncodeException.ErrorType int error, @Nullable String message,
+            @Nullable Throwable throwable) {
+        EncoderCallback encoderCallback = mEncoderCallback;
+        try {
+            mEncoderCallbackExecutor.execute(() -> encoderCallback.onEncodeError(
+                    new EncodeException(error, message, throwable)));
+        } catch (RejectedExecutionException re) {
+            Logger.e(TAG, "Unable to post to the supplied executor.", re);
+        }
+        mMediaCodec.stop();
+        handleStopped();
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @GuardedBy("mLock")
+    void handleStopped() {
+        if (mState == PENDING_RELEASE) {
+            mMediaCodec.release();
+            setState(RELEASED);
+        } else {
+            InternalState oldState = mState;
+            reset();
+            setState(CONFIGURED);
+            if (oldState == PENDING_START || oldState == PENDING_START_PAUSED) {
+                start();
+                if (oldState == PENDING_START_PAUSED && mState == STARTED) {
+                    pause();
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    static long generatePresentationTimeUs() {
+        return System.nanoTime() / 1000L;
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    class MediaCodecCallback extends MediaCodec.Callback {
+
+        @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+        @GuardedBy("mLock")
+        final Set<EncodedDataImpl> mEncodedDataSet = new HashSet<>();
+
+        @GuardedBy("mLock")
+        private boolean mHasFirstData = false;
+
+        @SuppressWarnings("GuardedBy")
+        // It complains ByteBufferInput#putFreeBufferIndex doesn't hold mLock
+        @Override
+        public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
+            synchronized (mLock) {
+                switch (mState) {
+                    case STARTED:
+                    case PAUSED:
+                    case STOPPING:
+                    case PENDING_START:
+                    case PENDING_START_PAUSED:
+                    case PENDING_RELEASE:
+                        if (mEncoderInput instanceof ByteBufferInput) {
+                            ((ByteBufferInput) mEncoderInput).putFreeBufferIndex(index);
+                        }
+                        break;
+                    case CONFIGURED:
+                    case RELEASED:
+                        // Do nothing
+                        break;
+                    default:
+                        throw new IllegalStateException("Unknown state: " + mState);
+                }
+            }
+        }
+
+        @Override
+        public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int index,
+                @NonNull MediaCodec.BufferInfo bufferInfo) {
+            synchronized (mLock) {
+                switch (mState) {
+                    case STARTED:
+                    case PAUSED:
+                    case STOPPING:
+                    case PENDING_START:
+                    case PENDING_START_PAUSED:
+                    case PENDING_RELEASE:
+                        final EncoderCallback encoderCallback = mEncoderCallback;
+                        final Executor executor = mEncoderCallbackExecutor;
+
+                        // Handle start of stream
+                        if (!mHasFirstData) {
+                            mHasFirstData = true;
+                            try {
+                                executor.execute(encoderCallback::onEncodeStart);
+                            } catch (RejectedExecutionException e) {
+                                Logger.e(TAG, "Unable to post to the supplied executor.", e);
+                            }
+                        }
+
+                        if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
+                            // The codec config data was sent out by MediaFormat when getting
+                            // onOutputFormatChanged(). Ignore it.
+                            bufferInfo.size = 0;
+                        }
+
+                        if (bufferInfo.size > 0) {
+                            if (mEncoderInput instanceof SurfaceInput) {
+                                // TODO(b/171972677): Overriding the video presentation time here
+                                //  may not be a right thing. It needs to do a translation to a
+                                //  common clock in order to keep video/audio in sync.
+                                bufferInfo.presentationTimeUs = generatePresentationTimeUs();
+                            }
+                            EncodedDataImpl encodedData;
+                            try {
+                                encodedData = new EncodedDataImpl(mediaCodec, index, bufferInfo);
+                            } catch (MediaCodec.CodecException e) {
+                                handleEncodeError(e);
+                                return;
+                            }
+                            // Propagate data
+                            mEncodedDataSet.add(encodedData);
+                            Futures.addCallback(encodedData.getClosedFuture(),
+                                    new FutureCallback<Void>() {
+                                        @Override
+                                        public void onSuccess(@Nullable Void result) {
+                                            synchronized (mLock) {
+                                                mEncodedDataSet.remove(encodedData);
+                                            }
+                                        }
+
+                                        @Override
+                                        public void onFailure(Throwable t) {
+                                            synchronized (mLock) {
+                                                mEncodedDataSet.remove(encodedData);
+                                                if (t instanceof MediaCodec.CodecException) {
+                                                    handleEncodeError(
+                                                            (MediaCodec.CodecException) t);
+                                                } else {
+                                                    handleEncodeError(EncodeException.ERROR_UNKNOWN,
+                                                            t.getMessage(), t);
+                                                }
+                                            }
+                                        }
+                                    }, CameraXExecutors.directExecutor());
+                            try {
+                                executor.execute(() -> encoderCallback.onEncodedData(encodedData));
+                            } catch (RejectedExecutionException e) {
+                                Logger.e(TAG, "Unable to post to the supplied executor.", e);
+                                encodedData.close();
+                            }
+                        } else {
+                            try {
+                                mMediaCodec.releaseOutputBuffer(index, false);
+                            } catch (MediaCodec.CodecException e) {
+                                handleEncodeError(e);
+                                return;
+                            }
+                        }
+
+                        // Handle end of stream
+                        if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+                            // Wait for all data closed
+                            List<ListenableFuture<Void>> waitForCloseFutures = new ArrayList<>();
+                            for (EncodedDataImpl dataToClose : mEncodedDataSet) {
+                                waitForCloseFutures.add(dataToClose.getClosedFuture());
+                            }
+                            Futures.addCallback(Futures.allAsList(waitForCloseFutures),
+                                    new FutureCallback<List<Void>>() {
+                                        @Override
+                                        public void onSuccess(@Nullable List<Void> result) {
+                                            synchronized (mLock) {
+                                                mMediaCodec.stop();
+                                                try {
+                                                    executor.execute(encoderCallback::onEncodeStop);
+                                                } catch (RejectedExecutionException e) {
+                                                    Logger.e(TAG,
+                                                            "Unable to post to the supplied "
+                                                                    + "executor.", e);
+                                                }
+                                                handleStopped();
+                                            }
+                                        }
+
+                                        @Override
+                                        public void onFailure(Throwable t) {
+                                            synchronized (mLock) {
+                                                if (t instanceof MediaCodec.CodecException) {
+                                                    handleEncodeError(
+                                                            (MediaCodec.CodecException) t);
+                                                } else {
+                                                    handleEncodeError(EncodeException.ERROR_UNKNOWN,
+                                                            t.getMessage(), t);
+                                                }
+                                            }
+                                        }
+                                    }, CameraXExecutors.directExecutor());
+                        }
+                        break;
+                    case CONFIGURED:
+                    case RELEASED:
+                        // Do nothing
+                        break;
+                    default:
+                        throw new IllegalStateException("Unknown state: " + mState);
+                }
+            }
+        }
+
+        @Override
+        public void onError(@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) {
+            synchronized (mLock) {
+                switch (mState) {
+                    case STARTED:
+                    case PAUSED:
+                    case STOPPING:
+                    case PENDING_START:
+                    case PENDING_START_PAUSED:
+                    case PENDING_RELEASE:
+                        handleEncodeError(e);
+                        break;
+                    case CONFIGURED:
+                    case RELEASED:
+                        // Do nothing
+                        break;
+                    default:
+                        throw new IllegalStateException("Unknown state: " + mState);
+                }
+            }
+        }
+
+        @Override
+        public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec,
+                @NonNull MediaFormat mediaFormat) {
+            synchronized (mLock) {
+                switch (mState) {
+                    case STARTED:
+                    case PAUSED:
+                    case STOPPING:
+                    case PENDING_START:
+                    case PENDING_START_PAUSED:
+                    case PENDING_RELEASE:
+                        EncoderCallback encoderCallback = mEncoderCallback;
+                        try {
+                            mEncoderCallbackExecutor.execute(
+                                    () -> encoderCallback.onOutputConfigUpdate(() -> mediaFormat));
+                        } catch (RejectedExecutionException e) {
+                            Logger.e(TAG, "Unable to post to the supplied executor.", e);
+                        }
+                        break;
+                    case CONFIGURED:
+                    case RELEASED:
+                        // Do nothing
+                        break;
+                    default:
+                        throw new IllegalStateException("Unknown state: " + mState);
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    class SurfaceInput implements Encoder.SurfaceInput {
+
+        @GuardedBy("mLock")
+        private Surface mSurface;
+
+        @GuardedBy("mLock")
+        private OnSurfaceUpdateListener mSurfaceUpdateListener;
+
+        @GuardedBy("mLock")
+        private Executor mSurfaceUpdateExecutor;
+
+        /**
+         * Sets the surface update listener.
+         *
+         * @param executor the executor to invoke the listener
+         * @param listener the surface update listener
+         */
+        @Override
+        public void setOnSurfaceUpdateListener(@NonNull Executor executor,
+                @NonNull OnSurfaceUpdateListener listener) {
+            synchronized (mLock) {
+                mSurfaceUpdateListener = Preconditions.checkNotNull(listener);
+                mSurfaceUpdateExecutor = Preconditions.checkNotNull(executor);
+
+                if (mSurface != null) {
+                    notifySurfaceUpdate(mSurface);
+                }
+
+            }
+        }
+
+        @GuardedBy("mLock")
+        @SuppressLint("UnsafeNewApiCall")
+        void resetSurface() {
+            if (Build.VERSION.SDK_INT >= 23) {
+                if (mSurface == null) {
+                    mSurface = MediaCodec.createPersistentInputSurface();
+                    notifySurfaceUpdate(mSurface);
+                }
+                mMediaCodec.setInputSurface(mSurface);
+            } else {
+                mSurface = mMediaCodec.createInputSurface();
+                notifySurfaceUpdate(mSurface);
+            }
+        }
+
+        @GuardedBy("mLock")
+        private void notifySurfaceUpdate(@NonNull Surface surface) {
+            if (mSurfaceUpdateListener != null && mSurfaceUpdateExecutor != null) {
+                OnSurfaceUpdateListener listener = mSurfaceUpdateListener;
+                try {
+                    mSurfaceUpdateExecutor.execute(() -> listener.onSurfaceUpdate(surface));
+                } catch (RejectedExecutionException e) {
+                    Logger.e(TAG, "Unable to post to the supplied executor.", e);
+                    surface.release();
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    class ByteBufferInput implements Encoder.ByteBufferInput {
+
+        @GuardedBy("mLock")
+        private final Queue<Consumer<Integer>> mListenerQueue = new ArrayDeque<>();
+
+        @GuardedBy("mLock")
+        private final Queue<Integer> mFreeBufferIndexQueue = new ArrayDeque<>();
+
+        /** {@inheritDoc} */
+        @Override
+        public void putByteBuffer(@NonNull ByteBuffer byteBuffer) {
+            synchronized (mLock) {
+                switch (mState) {
+                    case STARTED:
+                        // Here it means the byteBuffer should definitely be queued into codec.
+                        acquireFreeBufferIndex(freeBufferIndex -> {
+                            ByteBuffer inputBuffer = null;
+                            synchronized (mLock) {
+                                if (mState == STARTED
+                                        || mState == PAUSED
+                                        || mState == STOPPING
+                                        || mState == PENDING_START
+                                        || mState == PENDING_START_PAUSED
+                                        || mState == PENDING_RELEASE) {
+                                    try {
+                                        inputBuffer = mMediaCodec.getInputBuffer(freeBufferIndex);
+                                    } catch (MediaCodec.CodecException e) {
+                                        handleEncodeError(e);
+                                        return;
+                                    }
+                                }
+                            }
+
+                            if (inputBuffer == null) {
+                                return;
+                            }
+                            inputBuffer.put(byteBuffer);
+
+                            synchronized (mLock) {
+                                if (mState == STARTED
+                                        || mState == PAUSED
+                                        || mState == STOPPING
+                                        || mState == PENDING_START
+                                        || mState == PENDING_START_PAUSED
+                                        || mState == PENDING_RELEASE) {
+                                    try {
+                                        mMediaCodec.queueInputBuffer(freeBufferIndex, 0,
+                                                inputBuffer.position(),
+                                                generatePresentationTimeUs(),
+                                                0);
+                                    } catch (MediaCodec.CodecException e) {
+                                        handleEncodeError(e);
+                                        return;
+                                    }
+                                }
+                            }
+                        });
+                        break;
+                    case PAUSED:
+                        // Drop the data
+                        break;
+                    case CONFIGURED:
+                    case STOPPING:
+                    case PENDING_START:
+                    case PENDING_RELEASE:
+                    case RELEASED:
+                        // Do nothing
+                        break;
+                    default:
+                        throw new IllegalStateException("Unknown state: " + mState);
+                }
+            }
+        }
+
+        @GuardedBy("mLock")
+        void signalEndOfInputStream() {
+            acquireFreeBufferIndex(freeBufferIndex -> {
+                synchronized (mLock) {
+                    switch (mState) {
+                        case STARTED:
+                        case PAUSED:
+                        case STOPPING:
+                        case PENDING_START:
+                        case PENDING_START_PAUSED:
+                        case PENDING_RELEASE:
+                            try {
+                                mMediaCodec.queueInputBuffer(freeBufferIndex, 0, 0,
+                                        generatePresentationTimeUs(),
+                                        MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+                            } catch (MediaCodec.CodecException e) {
+                                handleEncodeError(e);
+                            }
+                            break;
+                        case CONFIGURED:
+                        case RELEASED:
+                            // Do nothing
+                            break;
+                        default:
+                            throw new IllegalStateException("Unknown state: " + mState);
+                    }
+                }
+            });
+        }
+
+        @GuardedBy("mLock")
+        void putFreeBufferIndex(int index) {
+            mFreeBufferIndexQueue.offer(index);
+            match();
+        }
+
+        @GuardedBy("mLock")
+        void clearFreeBuffers() {
+            mListenerQueue.clear();
+            mFreeBufferIndexQueue.clear();
+        }
+
+        @GuardedBy("mLock")
+        private void acquireFreeBufferIndex(
+                @NonNull Consumer<Integer> onFreeBufferIndexListener) {
+            synchronized (mLock) {
+                mListenerQueue.offer(onFreeBufferIndexListener);
+                match();
+            }
+        }
+
+        @GuardedBy("mLock")
+        private void match() {
+            if (!mListenerQueue.isEmpty() && !mFreeBufferIndexQueue.isEmpty()) {
+                Consumer<Integer> listener = mListenerQueue.poll();
+                Integer index = mFreeBufferIndexQueue.poll();
+                try {
+                    mExecutor.execute(() -> listener.accept(index));
+                } catch (RejectedExecutionException e) {
+                    Logger.e(TAG, "Unable to post to the supplied executor.", e);
+                    putFreeBufferIndex(index);
+                }
+            }
+        }
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InvalidConfigException.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InvalidConfigException.java
new file mode 100644
index 0000000..2264d71
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InvalidConfigException.java
@@ -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.video.internal.encoder;
+
+import androidx.annotation.Nullable;
+
+/** An exception thrown to indicate an error has occurred during configuring an encoder. */
+public class InvalidConfigException extends Exception {
+
+    public InvalidConfigException(@Nullable String message) {
+        super(message);
+    }
+
+    public InvalidConfigException(@Nullable String message, @Nullable Throwable cause) {
+        super(message, cause);
+    }
+
+    public InvalidConfigException(@Nullable Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/OutputConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/OutputConfig.java
new file mode 100644
index 0000000..c931eb0
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/OutputConfig.java
@@ -0,0 +1,34 @@
+/*
+ * 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.media.MediaFormat;
+
+import androidx.annotation.Nullable;
+
+/**
+ * The output config of an encoder.
+ *
+ * <p>The output config will be the final configuration of an {@link Encoder} relative to the
+ * input config {@link EncoderConfig}.
+ */
+public interface OutputConfig {
+
+    /** Gets the media format. */
+    @Nullable
+    MediaFormat getMediaFormat();
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/VideoEncoderConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/VideoEncoderConfig.java
new file mode 100644
index 0000000..ed838ca
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/VideoEncoderConfig.java
@@ -0,0 +1,110 @@
+/*
+ * 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.media.MediaFormat;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+/** {@inheritDoc} */
+@AutoValue
+public abstract class VideoEncoderConfig implements EncoderConfig {
+
+    // Restrict constructor to same package
+    VideoEncoderConfig() {
+    }
+
+    /** Returns a build for this config. */
+    @NonNull
+    public static Builder builder() {
+        return new AutoValue_VideoEncoderConfig.Builder();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @NonNull
+    public abstract String getMimeType();
+
+    /** Gets the resolution. */
+    @NonNull
+    public abstract Size getResolution();
+
+    /** Gets the color format. */
+    public abstract int getColorFormat();
+
+    /** Gets the frame rate. */
+    public abstract int getFrameRate();
+
+    /** Gets the i-frame interval. */
+    public abstract int getIFrameInterval();
+
+    /** Gets the bitrate. */
+    public abstract int getBitrate();
+
+    /** {@inheritDoc} */
+    @NonNull
+    @Override
+    public MediaFormat toMediaFormat() {
+        Size size = getResolution();
+        MediaFormat format = MediaFormat.createVideoFormat(getMimeType(), size.getWidth(),
+                size.getHeight());
+        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, getColorFormat());
+        format.setInteger(MediaFormat.KEY_BIT_RATE, getBitrate());
+        format.setInteger(MediaFormat.KEY_FRAME_RATE, getFrameRate());
+        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, getIFrameInterval());
+        return format;
+    }
+
+    /** The builder of the config. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+        // Restrict construction to same package
+        Builder() {
+        }
+
+        /** Sets the mime type. */
+        @NonNull
+        public abstract Builder setMimeType(@NonNull String mimeType);
+
+        /** Sets the resolution. */
+        @NonNull
+        public abstract Builder setResolution(@NonNull Size resolution);
+
+        /** Sets the color format. */
+        @NonNull
+        public abstract Builder setColorFormat(int colorFormat);
+
+        /** Sets the frame rate. */
+        @NonNull
+        public abstract Builder setFrameRate(int frameRate);
+
+        /** Sets the i-frame interval. */
+        @NonNull
+        public abstract Builder setIFrameInterval(int iFrameInterval);
+
+        /** Sets the bitrate. */
+        @NonNull
+        public abstract Builder setBitrate(int bitrate);
+
+        /** Builds the config instance. */
+        @NonNull
+        public abstract VideoEncoderConfig build();
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/package-info.java
similarity index 81%
copy from camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java
copy to camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/package-info.java
index d6b6436..a813f08 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -17,7 +17,7 @@
 /**
  * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-package androidx.camera.core.impl.quirk;
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.camera.video.internal.encoder;
 
 import androidx.annotation.RestrictTo;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/package-info.java
similarity index 81%
copy from camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java
copy to camera/camera-video/src/main/java/androidx/camera/video/internal/package-info.java
index d6b6436..d800e80 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -17,7 +17,7 @@
 /**
  * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-package androidx.camera.core.impl.quirk;
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.camera.video.internal;
 
 import androidx.annotation.RestrictTo;
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt
new file mode 100644
index 0000000..5c97071
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt
@@ -0,0 +1,91 @@
+/*
+ * 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
+
+import android.content.Context
+import android.os.Build
+import android.os.Looper
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.impl.CameraFactory
+import androidx.camera.core.impl.CameraThreadConfig
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.fakes.FakeAppConfig
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraFactory
+import androidx.test.core.app.ApplicationProvider
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import java.io.File
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(
+    minSdk = Build.VERSION_CODES.LOLLIPOP
+)
+class VideoCaptureLegacyTest {
+    @Before
+    fun setUp() {
+        val camera = FakeCamera()
+
+        val cameraFactoryProvider =
+            CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+                val cameraFactory = FakeCameraFactory()
+                cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
+                    camera
+                }
+                cameraFactory
+            }
+        val cameraXConfig = CameraXConfig.Builder.fromConfig(FakeAppConfig.create())
+            .setCameraFactoryProvider(cameraFactoryProvider)
+            .build()
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        CameraX.initialize(context, cameraXConfig).get()
+    }
+
+    @After
+    fun tearDown() {
+        CameraX.shutdown().get()
+    }
+
+    @Test
+    fun startRecording_beforeUseCaseIsBound() {
+        val videoCaptureLegacy = VideoCaptureLegacy.Builder().build()
+        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        val outputFileOptions = VideoCaptureLegacy.OutputFileOptions.Builder(file).build()
+        val callback = mock(VideoCaptureLegacy.OnVideoSavedCallback::class.java)
+        videoCaptureLegacy.startRecording(
+            outputFileOptions,
+            CameraXExecutors.mainThreadExecutor(),
+            callback
+        )
+        shadowOf(Looper.getMainLooper()).idle()
+
+        verify(callback).onError(eq(VideoCaptureLegacy.ERROR_INVALID_CAMERA), anyString(), any())
+    }
+}
\ No newline at end of file
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 8839e7a..a9eecc8 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -111,6 +111,7 @@
     method public final androidx.lifecycle.Lifecycle getLifecycle();
     method public String? getMarker();
     method public final androidx.car.app.ScreenManager getScreenManager();
+    method public abstract androidx.car.app.model.Template getTemplate();
     method public final void invalidate();
     method public void setMarker(String?);
     method public void setResult(Object?);
@@ -152,6 +153,919 @@
 
 }
 
+package androidx.car.app.model {
+
+  public final class Action {
+    method public static androidx.car.app.model.Action.Builder builder();
+    method public androidx.car.app.model.CarColor getBackgroundColor();
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public int getType();
+    method public boolean isStandard();
+    method public androidx.car.app.model.Action.Builder newBuilder();
+    method public static String typeToString(int);
+    field public static final androidx.car.app.model.Action APP_ICON;
+    field public static final androidx.car.app.model.Action BACK;
+    field public static final int TYPE_APP_ICON = 65538; // 0x10002
+    field public static final int TYPE_BACK = 65539; // 0x10003
+    field public static final int TYPE_CUSTOM = 1; // 0x1
+    field public static final int TYPE_UNKNOWN = 0; // 0x0
+  }
+
+  public static final class Action.Builder {
+    method public androidx.car.app.model.Action build();
+    method public androidx.car.app.model.Action.Builder setBackgroundColor(androidx.car.app.model.CarColor);
+    method public androidx.car.app.model.Action.Builder setIcon(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.model.Action.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+    method public androidx.car.app.model.Action.Builder setTitle(CharSequence?);
+  }
+
+  public class ActionList {
+    method public static androidx.car.app.model.ActionList create(java.util.List<androidx.car.app.model.Action!>);
+    method public java.util.List<androidx.car.app.model.Action!> getList();
+  }
+
+  public class ActionStrip {
+    method public static androidx.car.app.model.ActionStrip.Builder builder();
+    method public androidx.car.app.model.Action? getActionOfType(int);
+    method public java.util.List<java.lang.Object!> getActions();
+  }
+
+  public static final class ActionStrip.Builder {
+    ctor public ActionStrip.Builder();
+    method public androidx.car.app.model.ActionStrip.Builder addAction(androidx.car.app.model.Action);
+    method public androidx.car.app.model.ActionStrip build();
+    method public androidx.car.app.model.ActionStrip.Builder clearActions();
+  }
+
+  public class CarColor {
+    method public static androidx.car.app.model.CarColor createCustom(@ColorInt int, @ColorInt int);
+    method @ColorInt public int getColor();
+    method @ColorInt public int getColorDark();
+    method public int getType();
+    field public static final androidx.car.app.model.CarColor BLUE;
+    field public static final androidx.car.app.model.CarColor DEFAULT;
+    field public static final androidx.car.app.model.CarColor GREEN;
+    field public static final androidx.car.app.model.CarColor PRIMARY;
+    field public static final androidx.car.app.model.CarColor RED;
+    field public static final androidx.car.app.model.CarColor SECONDARY;
+    field public static final int TYPE_BLUE = 6; // 0x6
+    field public static final int TYPE_CUSTOM = 0; // 0x0
+    field public static final int TYPE_DEFAULT = 1; // 0x1
+    field public static final int TYPE_GREEN = 5; // 0x5
+    field public static final int TYPE_PRIMARY = 2; // 0x2
+    field public static final int TYPE_RED = 4; // 0x4
+    field public static final int TYPE_SECONDARY = 3; // 0x3
+    field public static final int TYPE_YELLOW = 7; // 0x7
+    field public static final androidx.car.app.model.CarColor YELLOW;
+  }
+
+  public class CarIcon {
+    method public static androidx.car.app.model.CarIcon.Builder builder(androidx.core.graphics.drawable.IconCompat);
+    method public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method public androidx.car.app.model.CarColor? getTint();
+    method public int getType();
+    method public androidx.car.app.model.CarIcon.Builder newBuilder();
+    method public static androidx.car.app.model.CarIcon of(androidx.core.graphics.drawable.IconCompat);
+    field public static final androidx.car.app.model.CarIcon ALERT;
+    field public static final androidx.car.app.model.CarIcon APP_ICON;
+    field public static final androidx.car.app.model.CarIcon BACK;
+    field public static final androidx.car.app.model.CarIcon ERROR;
+    field public static final int TYPE_ALERT = 4; // 0x4
+    field public static final int TYPE_APP = 5; // 0x5
+    field public static final int TYPE_BACK = 3; // 0x3
+    field public static final int TYPE_CUSTOM = 1; // 0x1
+    field public static final int TYPE_ERROR = 6; // 0x6
+    field public static final int TYPE_UNKNOWN = 0; // 0x0
+    field public static final int TYPE_WILLIAM_ALERT = 7; // 0x7
+    field public static final androidx.car.app.model.CarIcon WILLIAM_ALERT;
+  }
+
+  public static final class CarIcon.Builder {
+    method public androidx.car.app.model.CarIcon build();
+    method public androidx.car.app.model.CarIcon.Builder setIcon(androidx.car.app.model.CarIcon);
+    method public androidx.car.app.model.CarIcon.Builder setTint(androidx.car.app.model.CarColor?);
+  }
+
+  public class CarIconSpan extends android.text.style.CharacterStyle {
+    method public static androidx.car.app.model.CarIconSpan create(androidx.car.app.model.CarIcon);
+    method public static androidx.car.app.model.CarIconSpan create(androidx.car.app.model.CarIcon, int);
+    method public int getAlignment();
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public void updateDrawState(android.text.TextPaint?);
+    method public static int validateAlignment(int);
+    field public static final int ALIGN_BASELINE = 1; // 0x1
+    field public static final int ALIGN_BOTTOM = 0; // 0x0
+    field public static final int ALIGN_CENTER = 2; // 0x2
+  }
+
+  public class CarText {
+    ctor public CarText();
+    method public static androidx.car.app.model.CarText create(CharSequence);
+    method public java.util.List<androidx.car.app.model.CarText.SpanWrapper!> getSpans();
+    method public String? getText();
+    method public boolean isEmpty();
+    method public static boolean isNullOrEmpty(androidx.car.app.model.CarText?);
+    method public static String? toShortString(androidx.car.app.model.CarText?);
+    field public static final androidx.car.app.model.CarText EMPTY;
+  }
+
+  public static class CarText.SpanWrapper {
+    field @Keep public final int end;
+    field @Keep public final int flags;
+    field @Keep public final Object? span;
+    field @Keep public final int start;
+  }
+
+  public class DateTimeWithZone {
+    method public static androidx.car.app.model.DateTimeWithZone create(long, int, String);
+    method public static androidx.car.app.model.DateTimeWithZone create(long, java.util.TimeZone);
+    method @RequiresApi(26) public static androidx.car.app.model.DateTimeWithZone create(java.time.ZonedDateTime);
+    method public long getTimeSinceEpochMillis();
+    method public int getZoneOffsetSeconds();
+    method public String? getZoneShortName();
+  }
+
+  public final class Distance {
+    method public static androidx.car.app.model.Distance create(double, int);
+    method public double getDisplayDistance();
+    method public int getDisplayUnit();
+    field public static final int UNIT_FEET = 6; // 0x6
+    field public static final int UNIT_KILOMETERS = 2; // 0x2
+    field public static final int UNIT_KILOMETERS_P1 = 3; // 0x3
+    field public static final int UNIT_METERS = 1; // 0x1
+    field public static final int UNIT_MILES = 4; // 0x4
+    field public static final int UNIT_MILES_P1 = 5; // 0x5
+    field public static final int UNIT_YARDS = 7; // 0x7
+  }
+
+  public class DistanceSpan extends android.text.style.CharacterStyle {
+    method public static androidx.car.app.model.DistanceSpan create(androidx.car.app.model.Distance);
+    method public androidx.car.app.model.Distance getDistance();
+    method public void updateDrawState(android.text.TextPaint?);
+  }
+
+  public class DurationSpan extends android.text.style.CharacterStyle {
+    method public static androidx.car.app.model.DurationSpan create(long);
+    method @RequiresApi(26) public static androidx.car.app.model.DurationSpan create(java.time.Duration);
+    method public long getDurationSeconds();
+    method public void updateDrawState(android.text.TextPaint?);
+  }
+
+  public class ForegroundCarColorSpan extends android.text.style.CharacterStyle {
+    method public static androidx.car.app.model.ForegroundCarColorSpan create(androidx.car.app.model.CarColor);
+    method public androidx.car.app.model.CarColor getColor();
+    method public void updateDrawState(android.text.TextPaint);
+  }
+
+  public class GridItem implements androidx.car.app.model.Item {
+    method public static androidx.car.app.model.GridItem.Builder builder();
+    method public androidx.car.app.model.CarIcon getImage();
+    method public int getImageType();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.CarText? getText();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public androidx.car.app.model.Toggle? getToggle();
+    field public static final int IMAGE_TYPE_ICON = 1; // 0x1
+    field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
+  }
+
+  public static final class GridItem.Builder {
+    method public androidx.car.app.model.GridItem build();
+    method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon);
+    method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
+    method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+    method public androidx.car.app.model.GridItem.Builder setText(CharSequence?);
+    method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
+    method public androidx.car.app.model.GridItem.Builder setToggle(androidx.car.app.model.Toggle?);
+  }
+
+  public final class GridTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.GridTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.CarIcon? getBackgroundImage();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.ItemList? getSingleList();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isLoading();
+  }
+
+  public static final class GridTemplate.Builder {
+    method public androidx.car.app.model.GridTemplate build();
+    method public androidx.car.app.model.GridTemplate.Builder clearAllLists();
+    method public androidx.car.app.model.GridTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.GridTemplate.Builder setBackgroundImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.model.GridTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.GridTemplate.Builder setLoading(boolean);
+    method public androidx.car.app.model.GridTemplate.Builder setSingleList(androidx.car.app.model.ItemList);
+    method public androidx.car.app.model.GridTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public interface Item {
+  }
+
+  public final class ItemList {
+    method public static androidx.car.app.model.ItemList.Builder builder();
+    method public java.util.List<java.lang.Object!> getItems();
+    method public androidx.car.app.model.CarText? getNoItemsMessage();
+    method public int getSelectedIndex();
+    method public boolean isRefresh(androidx.car.app.model.ItemList?, androidx.car.app.utils.Logger);
+  }
+
+  public static final class ItemList.Builder {
+    ctor public ItemList.Builder();
+    method public androidx.car.app.model.ItemList.Builder addItem(androidx.car.app.model.Item);
+    method public androidx.car.app.model.ItemList build();
+    method public androidx.car.app.model.ItemList.Builder clearItems();
+    method public androidx.car.app.model.ItemList.Builder setNoItemsMessage(CharSequence?);
+    method public androidx.car.app.model.ItemList.Builder setOnItemsVisibilityChangeListener(androidx.car.app.model.ItemList.OnItemVisibilityChangedListener?);
+    method public androidx.car.app.model.ItemList.Builder setSelectable(androidx.car.app.model.ItemList.OnSelectedListener?);
+    method public androidx.car.app.model.ItemList.Builder setSelectedIndex(int);
+  }
+
+  public static interface ItemList.OnItemVisibilityChangedListener {
+    method public void onItemVisibilityChanged(int, int);
+  }
+
+  public static interface ItemList.OnSelectedListener {
+    method public void onSelected(int);
+  }
+
+  public final class LatLng {
+    method public static androidx.car.app.model.LatLng create(double, double);
+    method public static androidx.car.app.model.LatLng create(android.location.Location);
+    method public double getLatitude();
+    method public double getLongitude();
+  }
+
+  public final class ListTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.ListTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public java.util.List<androidx.car.app.model.SectionedItemList!> getSectionLists();
+    method public androidx.car.app.model.ItemList? getSingleList();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isLoading();
+  }
+
+  public static final class ListTemplate.Builder {
+    method public androidx.car.app.model.ListTemplate.Builder addList(androidx.car.app.model.ItemList, CharSequence);
+    method public androidx.car.app.model.ListTemplate build();
+    method public androidx.car.app.model.ListTemplate.Builder clearAllLists();
+    method public androidx.car.app.model.ListTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.ListTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.ListTemplate.Builder setLoading(boolean);
+    method public androidx.car.app.model.ListTemplate.Builder setSingleList(androidx.car.app.model.ItemList);
+    method public androidx.car.app.model.ListTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public final class MessageTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.MessageTemplate.Builder builder(CharSequence);
+    method public androidx.car.app.model.ActionList? getActionList();
+    method public androidx.car.app.model.CarText? getDebugMessage();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public androidx.car.app.model.CarText getMessage();
+    method public androidx.car.app.model.CarText? getTitle();
+  }
+
+  public static final class MessageTemplate.Builder {
+    method public androidx.car.app.model.MessageTemplate build();
+    method public androidx.car.app.model.MessageTemplate.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+    method public androidx.car.app.model.MessageTemplate.Builder setDebugCause(Throwable?);
+    method public androidx.car.app.model.MessageTemplate.Builder setDebugMessage(String?);
+    method public androidx.car.app.model.MessageTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.MessageTemplate.Builder setIcon(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.model.MessageTemplate.Builder setMessage(CharSequence);
+    method public androidx.car.app.model.MessageTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public class Metadata {
+    method public static androidx.car.app.model.Metadata.Builder builder();
+    method public androidx.car.app.model.Place? getPlace();
+    method public androidx.car.app.model.Metadata.Builder newBuilder();
+    method public static androidx.car.app.model.Metadata ofPlace(androidx.car.app.model.Place);
+    field public static final androidx.car.app.model.Metadata EMPTY_METADATA;
+  }
+
+  public static final class Metadata.Builder {
+    method public androidx.car.app.model.Metadata build();
+    method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
+  }
+
+  public interface OnClickListener {
+    method public void onClick();
+  }
+
+  public class OnClickListenerWrapper {
+    method public boolean isParkedOnly();
+  }
+
+  public final class Pane {
+    method public static androidx.car.app.model.Pane.Builder builder();
+    method public androidx.car.app.model.ActionList? getActionList();
+    method public java.util.List<java.lang.Object!> getRows();
+    method public boolean isLoading();
+    method public boolean isRefresh(androidx.car.app.model.Pane?, androidx.car.app.utils.Logger);
+  }
+
+  public static final class Pane.Builder {
+    ctor public Pane.Builder();
+    method public androidx.car.app.model.Pane.Builder addRow(androidx.car.app.model.Row);
+    method public androidx.car.app.model.Pane build();
+    method public androidx.car.app.model.Pane.Builder clearRows();
+    method public androidx.car.app.model.Pane.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+    method public androidx.car.app.model.Pane.Builder setLoading(boolean);
+  }
+
+  public final class PaneTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.PaneTemplate.Builder builder(androidx.car.app.model.Pane);
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.Pane getPane();
+    method public androidx.car.app.model.CarText? getTitle();
+  }
+
+  public static final class PaneTemplate.Builder {
+    method public androidx.car.app.model.PaneTemplate build();
+    method public androidx.car.app.model.PaneTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.PaneTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.PaneTemplate.Builder setPane(androidx.car.app.model.Pane);
+    method public androidx.car.app.model.PaneTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public final class ParkedOnlyOnClickListener implements androidx.car.app.model.OnClickListener {
+    method public static androidx.car.app.model.ParkedOnlyOnClickListener create(androidx.car.app.model.OnClickListener);
+    method public void onClick();
+  }
+
+  public class Place {
+    method public static androidx.car.app.model.Place.Builder builder(androidx.car.app.model.LatLng);
+    method public androidx.car.app.model.LatLng getLatLng();
+    method public androidx.car.app.model.PlaceMarker? getMarker();
+    method public androidx.car.app.model.Place.Builder newBuilder();
+  }
+
+  public static final class Place.Builder {
+    method public androidx.car.app.model.Place build();
+    method public androidx.car.app.model.Place.Builder setLatLng(androidx.car.app.model.LatLng);
+    method public androidx.car.app.model.Place.Builder setMarker(androidx.car.app.model.PlaceMarker?);
+  }
+
+  public final class PlaceListMapTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.PlaceListMapTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Place? getAnchor();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.ItemList? getItemList();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isCurrentLocationEnabled();
+    method public boolean isLoading();
+  }
+
+  public static final class PlaceListMapTemplate.Builder {
+    ctor public PlaceListMapTemplate.Builder();
+    method public androidx.car.app.model.PlaceListMapTemplate build();
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setAnchor(androidx.car.app.model.Place?);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setCurrentLocationEnabled(boolean);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setLoading(boolean);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public class PlaceMarker {
+    method public static androidx.car.app.model.PlaceMarker.Builder builder();
+    method public androidx.car.app.model.CarColor? getColor();
+    method public static androidx.car.app.model.PlaceMarker getDefault();
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public int getIconType();
+    method public androidx.car.app.model.CarText? getLabel();
+    method public static boolean isDefaultMarker(androidx.car.app.model.PlaceMarker?);
+    field public static final int TYPE_ICON = 0; // 0x0
+    field public static final int TYPE_IMAGE = 1; // 0x1
+  }
+
+  public static final class PlaceMarker.Builder {
+    method public androidx.car.app.model.PlaceMarker build();
+    method public androidx.car.app.model.PlaceMarker.Builder setColor(androidx.car.app.model.CarColor?);
+    method public androidx.car.app.model.PlaceMarker.Builder setIcon(androidx.car.app.model.CarIcon?, int);
+    method public androidx.car.app.model.PlaceMarker.Builder setLabel(CharSequence?);
+  }
+
+  public class Row implements androidx.car.app.model.Item {
+    method public static androidx.car.app.model.Row.Builder builder();
+    method public int getFlags();
+    method public androidx.car.app.model.CarIcon? getImage();
+    method public androidx.car.app.model.Metadata getMetadata();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+    method public int getRowImageType();
+    method public java.util.List<androidx.car.app.model.CarText!> getTexts();
+    method public androidx.car.app.model.CarText getTitle();
+    method public androidx.car.app.model.Toggle? getToggle();
+    method public boolean isBrowsable();
+    method public androidx.car.app.model.Row row();
+    method public void yourBoat();
+    field public static final int IMAGE_TYPE_ICON = 4; // 0x4
+    field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
+    field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
+    field public static final int ROW_FLAG_NONE = 1; // 0x1
+    field public static final int ROW_FLAG_SECTION_HEADER = 4; // 0x4
+    field public static final int ROW_FLAG_SHOW_DIVIDERS = 2; // 0x2
+  }
+
+  public static final class Row.Builder {
+    method public androidx.car.app.model.Row.Builder addText(CharSequence);
+    method public androidx.car.app.model.Row build();
+    method public androidx.car.app.model.Row.Builder clearText();
+    method public androidx.car.app.model.Row.Builder setBrowsable(boolean);
+    method public androidx.car.app.model.Row.Builder setFlags(int);
+    method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?, int);
+    method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
+    method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+    method public androidx.car.app.model.Row.Builder setTitle(CharSequence);
+    method public androidx.car.app.model.Row.Builder setToggle(androidx.car.app.model.Toggle?);
+  }
+
+  public final class SearchTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.SearchListener);
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public String? getInitialSearchText();
+    method public androidx.car.app.model.ItemList? getItemList();
+    method public String? getSearchHint();
+    method public boolean isLoading();
+    method public boolean isShowKeyboardByDefault();
+  }
+
+  public static final class SearchTemplate.Builder {
+    method public androidx.car.app.model.SearchTemplate build();
+    method public androidx.car.app.model.SearchTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.SearchTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.SearchTemplate.Builder setInitialSearchText(String?);
+    method public androidx.car.app.model.SearchTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.model.SearchTemplate.Builder setLoading(boolean);
+    method public androidx.car.app.model.SearchTemplate.Builder setSearchHint(String?);
+    method public androidx.car.app.model.SearchTemplate.Builder setShowKeyboardByDefault(boolean);
+  }
+
+  public class SectionedItemList {
+    method public static androidx.car.app.model.SectionedItemList create(androidx.car.app.model.ItemList, androidx.car.app.model.CarText);
+    method public androidx.car.app.model.CarText getHeader();
+    method public androidx.car.app.model.ItemList getItemList();
+  }
+
+  public interface Template {
+    method public default void checkPermissions(android.content.Context);
+    method public default boolean isRefresh(androidx.car.app.model.Template, androidx.car.app.utils.Logger);
+  }
+
+  public final class TemplateInfo {
+    ctor public TemplateInfo(androidx.car.app.model.Template, String);
+    method public Class<? extends androidx.car.app.model.Template> getTemplateClass();
+    method public String getTemplateId();
+  }
+
+  public final class TemplateWrapper {
+    method public static androidx.car.app.model.TemplateWrapper copyOf(androidx.car.app.model.TemplateWrapper);
+    method public int getCurrentTaskStep();
+    method public String getId();
+    method public androidx.car.app.model.Template getTemplate();
+    method public java.util.List<androidx.car.app.model.TemplateInfo!>? getTemplateInfosForScreenStack();
+    method public boolean isRefresh();
+    method public void setCurrentTaskStep(int);
+    method public void setId(String);
+    method public void setRefresh(boolean);
+    method public void setTemplate(androidx.car.app.model.Template);
+    method public static androidx.car.app.model.TemplateWrapper wrap(androidx.car.app.model.Template);
+    method public static androidx.car.app.model.TemplateWrapper wrap(androidx.car.app.model.Template, String);
+  }
+
+  public class Toggle {
+    method public static androidx.car.app.model.Toggle.Builder builder(androidx.car.app.model.Toggle.OnCheckedChangeListener);
+    method public boolean isChecked();
+  }
+
+  public static final class Toggle.Builder {
+    method public androidx.car.app.model.Toggle build();
+    method public androidx.car.app.model.Toggle.Builder setChecked(boolean);
+    method public androidx.car.app.model.Toggle.Builder setCheckedChangeListener(androidx.car.app.model.Toggle.OnCheckedChangeListener);
+  }
+
+  public static interface Toggle.OnCheckedChangeListener {
+    method public void onCheckedChange(boolean);
+  }
+
+}
+
+package androidx.car.app.model.constraints {
+
+  public class ActionsConstraints {
+    method @VisibleForTesting public static androidx.car.app.model.constraints.ActionsConstraints.Builder builder();
+    method public java.util.Set<java.lang.Integer!> getDisallowedActionTypes();
+    method public int getMaxActions();
+    method public int getMaxCustomTitles();
+    method public java.util.Set<java.lang.Integer!> getRequiredActionTypes();
+    method @VisibleForTesting public androidx.car.app.model.constraints.ActionsConstraints.Builder newBuilder();
+    method public void validateOrThrow(java.util.List<java.lang.Object!>);
+    field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_HEADER;
+    field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION;
+    field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_SIMPLE;
+  }
+
+  @VisibleForTesting public static final class ActionsConstraints.Builder {
+    method public androidx.car.app.model.constraints.ActionsConstraints.Builder addDisallowedActionType(int);
+    method public androidx.car.app.model.constraints.ActionsConstraints.Builder addRequiredActionType(int);
+    method public androidx.car.app.model.constraints.ActionsConstraints build();
+    method public androidx.car.app.model.constraints.ActionsConstraints.Builder setMaxActions(int);
+    method public androidx.car.app.model.constraints.ActionsConstraints.Builder setMaxCustomTitles(int);
+  }
+
+  public class CarColorConstraints {
+    method public void validateOrThrow(androidx.car.app.model.CarColor);
+    field public static final androidx.car.app.model.constraints.CarColorConstraints STANDARD_ONLY;
+    field public static final androidx.car.app.model.constraints.CarColorConstraints UNCONSTRAINED;
+  }
+
+  public class CarIconConstraints {
+    method public androidx.core.graphics.drawable.IconCompat checkSupportedIcon(androidx.core.graphics.drawable.IconCompat);
+    method public void validateOrThrow(androidx.car.app.model.CarIcon?);
+    field public static final androidx.car.app.model.constraints.CarIconConstraints DEFAULT;
+    field public static final androidx.car.app.model.constraints.CarIconConstraints UNCONSTRAINED;
+  }
+
+  public class RowConstraints {
+    method public static androidx.car.app.model.constraints.RowConstraints.Builder builder();
+    method public androidx.car.app.model.constraints.CarIconConstraints getCarIconConstraints();
+    method public int getFlagOverrides();
+    method public int getMaxActionsExclusive();
+    method public int getMaxTextLinesPerRow();
+    method public boolean isImageAllowed();
+    method public boolean isOnClickListenerAllowed();
+    method public boolean isToggleAllowed();
+    method public androidx.car.app.model.constraints.RowConstraints.Builder newBuilder();
+    method public void validateOrThrow(Object);
+    field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_CONSERVATIVE;
+    field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_FULL_LIST;
+    field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_PANE;
+    field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_SIMPLE;
+    field public static final androidx.car.app.model.constraints.RowConstraints UNCONSTRAINED;
+  }
+
+  public static final class RowConstraints.Builder {
+    method public androidx.car.app.model.constraints.RowConstraints build();
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setCarIconConstraints(androidx.car.app.model.constraints.CarIconConstraints);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setFlagOverrides(int);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setImageAllowed(boolean);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxActionsExclusive(int);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxTextLinesPerRow(int);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setOnClickListenerAllowed(boolean);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setToggleAllowed(boolean);
+  }
+
+  public class RowListConstraints {
+    method public static androidx.car.app.model.constraints.RowListConstraints.Builder builder();
+    method public int getMaxActions();
+    method public androidx.car.app.model.constraints.RowConstraints getRowConstraints();
+    method public int getRowListType();
+    method public boolean isAllowSelectableLists();
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder newBuilder();
+    method public void validateOrThrow(androidx.car.app.model.ItemList);
+    method public void validateOrThrow(java.util.List<androidx.car.app.model.SectionedItemList!>);
+    method public void validateOrThrow(androidx.car.app.model.Pane);
+    field public static final int DEFAULT_LIST = 0; // 0x0
+    field public static final int PANE = 1; // 0x1
+    field public static final int ROUTE_PREVIEW = 2; // 0x2
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_CONSERVATIVE;
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_FULL_LIST;
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_PANE;
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW;
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_SIMPLE;
+  }
+
+  public static final class RowListConstraints.Builder {
+    method public androidx.car.app.model.constraints.RowListConstraints build();
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder setAllowSelectableLists(boolean);
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder setMaxActions(int);
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder setRowConstraints(androidx.car.app.model.constraints.RowConstraints);
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder setRowListType(int);
+  }
+
+}
+
+package androidx.car.app.navigation {
+
+  public class NavigationManager {
+    method @MainThread public void navigationEnded();
+    method @MainThread public void navigationStarted();
+    method @MainThread public void setListener(androidx.car.app.navigation.NavigationManagerListener?);
+    method @MainThread public void updateTrip(androidx.car.app.navigation.model.Trip);
+  }
+
+  public interface NavigationManagerListener {
+    method public void onAutoDriveEnabled();
+    method public void stopNavigation();
+  }
+
+}
+
+package androidx.car.app.navigation.model {
+
+  public final class Destination {
+    method public static androidx.car.app.navigation.model.Destination.Builder builder(CharSequence, CharSequence);
+    method public static androidx.car.app.navigation.model.Destination.Builder builder();
+    method public androidx.car.app.model.CarText? getAddress();
+    method public androidx.car.app.model.CarIcon? getImage();
+    method public androidx.car.app.model.CarText? getName();
+  }
+
+  public static final class Destination.Builder {
+    method public androidx.car.app.navigation.model.Destination build();
+    method public androidx.car.app.navigation.model.Destination.Builder setAddress(CharSequence?);
+    method public androidx.car.app.navigation.model.Destination.Builder setImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.Destination.Builder setName(CharSequence?);
+  }
+
+  public final class Lane {
+    method public static androidx.car.app.navigation.model.Lane.Builder builder();
+    method public java.util.List<androidx.car.app.navigation.model.LaneDirection!> getDirections();
+  }
+
+  public static final class Lane.Builder {
+    ctor public Lane.Builder();
+    method public androidx.car.app.navigation.model.Lane.Builder addDirection(androidx.car.app.navigation.model.LaneDirection);
+    method public androidx.car.app.navigation.model.Lane build();
+    method public androidx.car.app.navigation.model.Lane.Builder clearDirections();
+  }
+
+  public final class LaneDirection {
+    method public static androidx.car.app.navigation.model.LaneDirection create(int, boolean);
+    method public int getShape();
+    method public boolean isHighlighted();
+    field public static final int SHAPE_NORMAL_LEFT = 5; // 0x5
+    field public static final int SHAPE_NORMAL_RIGHT = 6; // 0x6
+    field public static final int SHAPE_SHARP_LEFT = 7; // 0x7
+    field public static final int SHAPE_SHARP_RIGHT = 8; // 0x8
+    field public static final int SHAPE_SLIGHT_LEFT = 3; // 0x3
+    field public static final int SHAPE_SLIGHT_RIGHT = 4; // 0x4
+    field public static final int SHAPE_STRAIGHT = 2; // 0x2
+    field public static final int SHAPE_UNKNOWN = 1; // 0x1
+    field public static final int SHAPE_U_TURN_LEFT = 9; // 0x9
+    field public static final int SHAPE_U_TURN_RIGHT = 10; // 0xa
+  }
+
+  public final class Maneuver {
+    method public static androidx.car.app.navigation.model.Maneuver.Builder builder(int);
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public int getRoundaboutExitAngle();
+    method public int getRoundaboutExitNumber();
+    method public int getType();
+    field public static final int TYPE_DEPART = 1; // 0x1
+    field public static final int TYPE_DESTINATION = 39; // 0x27
+    field public static final int TYPE_DESTINATION_LEFT = 41; // 0x29
+    field public static final int TYPE_DESTINATION_RIGHT = 42; // 0x2a
+    field public static final int TYPE_DESTINATION_STRAIGHT = 40; // 0x28
+    field public static final int TYPE_FERRY_BOAT = 37; // 0x25
+    field public static final int TYPE_FERRY_TRAIN = 38; // 0x26
+    field public static final int TYPE_FORK_LEFT = 25; // 0x19
+    field public static final int TYPE_FORK_RIGHT = 26; // 0x1a
+    field public static final int TYPE_KEEP_LEFT = 3; // 0x3
+    field public static final int TYPE_KEEP_RIGHT = 4; // 0x4
+    field public static final int TYPE_MERGE_LEFT = 27; // 0x1b
+    field public static final int TYPE_MERGE_RIGHT = 28; // 0x1c
+    field public static final int TYPE_MERGE_SIDE_UNSPECIFIED = 29; // 0x1d
+    field public static final int TYPE_NAME_CHANGE = 2; // 0x2
+    field public static final int TYPE_OFF_RAMP_NORMAL_LEFT = 23; // 0x17
+    field public static final int TYPE_OFF_RAMP_NORMAL_RIGHT = 24; // 0x18
+    field public static final int TYPE_OFF_RAMP_SLIGHT_LEFT = 21; // 0x15
+    field public static final int TYPE_OFF_RAMP_SLIGHT_RIGHT = 22; // 0x16
+    field public static final int TYPE_ON_RAMP_NORMAL_LEFT = 15; // 0xf
+    field public static final int TYPE_ON_RAMP_NORMAL_RIGHT = 16; // 0x10
+    field public static final int TYPE_ON_RAMP_SHARP_LEFT = 17; // 0x11
+    field public static final int TYPE_ON_RAMP_SHARP_RIGHT = 18; // 0x12
+    field public static final int TYPE_ON_RAMP_SLIGHT_LEFT = 13; // 0xd
+    field public static final int TYPE_ON_RAMP_SLIGHT_RIGHT = 14; // 0xe
+    field public static final int TYPE_ON_RAMP_U_TURN_LEFT = 19; // 0x13
+    field public static final int TYPE_ON_RAMP_U_TURN_RIGHT = 20; // 0x14
+    field public static final int TYPE_ROUNDABOUT_ENTER = 30; // 0x1e
+    field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW = 34; // 0x22
+    field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE = 35; // 0x23
+    field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW = 32; // 0x20
+    field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE = 33; // 0x21
+    field public static final int TYPE_ROUNDABOUT_EXIT = 31; // 0x1f
+    field public static final int TYPE_STRAIGHT = 36; // 0x24
+    field public static final int TYPE_TURN_NORMAL_LEFT = 7; // 0x7
+    field public static final int TYPE_TURN_NORMAL_RIGHT = 8; // 0x8
+    field public static final int TYPE_TURN_SHARP_LEFT = 9; // 0x9
+    field public static final int TYPE_TURN_SHARP_RIGHT = 10; // 0xa
+    field public static final int TYPE_TURN_SLIGHT_LEFT = 5; // 0x5
+    field public static final int TYPE_TURN_SLIGHT_RIGHT = 6; // 0x6
+    field public static final int TYPE_UNKNOWN = 0; // 0x0
+    field public static final int TYPE_U_TURN_LEFT = 11; // 0xb
+    field public static final int TYPE_U_TURN_RIGHT = 12; // 0xc
+  }
+
+  public static final class Maneuver.Builder {
+    method public androidx.car.app.navigation.model.Maneuver build();
+    method public androidx.car.app.navigation.model.Maneuver.Builder setIcon(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.Maneuver.Builder setRoundaboutExitAngle(int);
+    method public androidx.car.app.navigation.model.Maneuver.Builder setRoundaboutExitNumber(int);
+  }
+
+  public class MessageInfo implements androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo {
+    method public static androidx.car.app.navigation.model.MessageInfo.Builder builder(CharSequence);
+    method public androidx.car.app.model.CarIcon? getImage();
+    method public androidx.car.app.model.CarText? getText();
+    method public androidx.car.app.model.CarText getTitle();
+  }
+
+  public static final class MessageInfo.Builder {
+    method public androidx.car.app.navigation.model.MessageInfo build();
+    method public androidx.car.app.navigation.model.MessageInfo.Builder setImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.MessageInfo.Builder setText(CharSequence?);
+    method public androidx.car.app.navigation.model.MessageInfo.Builder setTitle(CharSequence);
+  }
+
+  public class NavigationTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.navigation.model.NavigationTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip getActionStrip();
+    method public androidx.car.app.model.CarColor? getBackgroundColor();
+    method public androidx.car.app.navigation.model.TravelEstimate? getDestinationTravelEstimate();
+    method public androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo? getNavigationInfo();
+  }
+
+  public static final class NavigationTemplate.Builder {
+    method public androidx.car.app.navigation.model.NavigationTemplate build();
+    method public androidx.car.app.navigation.model.NavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip);
+    method public androidx.car.app.navigation.model.NavigationTemplate.Builder setBackgroundColor(androidx.car.app.model.CarColor?);
+    method public androidx.car.app.navigation.model.NavigationTemplate.Builder setDestinationTravelEstimate(androidx.car.app.navigation.model.TravelEstimate?);
+    method public androidx.car.app.navigation.model.NavigationTemplate.Builder setNavigationInfo(androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo?);
+  }
+
+  public static interface NavigationTemplate.NavigationInfo {
+  }
+
+  public final class PlaceListNavigationTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.ItemList? getItemList();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isLoading();
+  }
+
+  public static final class PlaceListNavigationTemplate.Builder {
+    ctor public PlaceListNavigationTemplate.Builder();
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate build();
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setIsLoading(boolean);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+    method @VisibleForTesting public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public final class RoutePreviewNavigationTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.ItemList? getItemList();
+    method public androidx.car.app.model.Action? getNavigateAction();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isLoading();
+  }
+
+  public static final class RoutePreviewNavigationTemplate.Builder {
+    ctor public RoutePreviewNavigationTemplate.Builder();
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate build();
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setIsLoading(boolean);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+    method @VisibleForTesting public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public class RoutingInfo implements androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo {
+    method public static androidx.car.app.navigation.model.RoutingInfo.Builder builder();
+    method public androidx.car.app.model.Distance? getCurrentDistance();
+    method public androidx.car.app.navigation.model.Step? getCurrentStep();
+    method public androidx.car.app.model.CarIcon? getJunctionImage();
+    method public androidx.car.app.navigation.model.Step? getNextStep();
+    method public boolean isLoading();
+  }
+
+  public static final class RoutingInfo.Builder {
+    method public androidx.car.app.navigation.model.RoutingInfo build();
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setCurrentStep(androidx.car.app.navigation.model.Step, androidx.car.app.model.Distance);
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setIsLoading(boolean);
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setJunctionImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setNextStep(androidx.car.app.navigation.model.Step?);
+  }
+
+  public final class Step {
+    method public static androidx.car.app.navigation.model.Step.Builder builder(CharSequence);
+    method public androidx.car.app.model.CarText? getCue();
+    method public java.util.List<androidx.car.app.navigation.model.Lane!> getLanes();
+    method public androidx.car.app.model.CarIcon? getLanesImage();
+    method public androidx.car.app.navigation.model.Maneuver? getManeuver();
+    method public androidx.car.app.model.CarText? getRoad();
+    method public androidx.car.app.navigation.model.Step.Builder newBuilder();
+  }
+
+  public static final class Step.Builder {
+    method public androidx.car.app.navigation.model.Step.Builder addLane(androidx.car.app.navigation.model.Lane);
+    method public androidx.car.app.navigation.model.Step build();
+    method public androidx.car.app.navigation.model.Step.Builder clearLanes();
+    method public androidx.car.app.navigation.model.Step.Builder setCue(CharSequence);
+    method public androidx.car.app.navigation.model.Step.Builder setLanesImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.Step.Builder setManeuver(androidx.car.app.navigation.model.Maneuver?);
+    method public androidx.car.app.navigation.model.Step.Builder setRoad(CharSequence);
+  }
+
+  public final class TravelEstimate {
+    method public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
+    method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+    method public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
+    method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+    method public androidx.car.app.model.DateTimeWithZone? getArrivalTimeAtDestination();
+    method public androidx.car.app.model.Distance getRemainingDistance();
+    method public androidx.car.app.model.CarColor getRemainingDistanceColor();
+    method public androidx.car.app.model.CarColor getRemainingTimeColor();
+    method public long getRemainingTimeSeconds();
+  }
+
+  public static final class TravelEstimate.Builder {
+    method public androidx.car.app.navigation.model.TravelEstimate build();
+    method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingDistanceColor(androidx.car.app.model.CarColor);
+    method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTimeColor(androidx.car.app.model.CarColor);
+  }
+
+  public final class Trip {
+    method public static androidx.car.app.navigation.model.Trip.Builder builder();
+    method public androidx.car.app.model.CarText? getCurrentRoad();
+    method public java.util.List<androidx.car.app.navigation.model.TravelEstimate!> getDestinationTravelEstimates();
+    method public java.util.List<androidx.car.app.navigation.model.Destination!> getDestinations();
+    method public java.util.List<androidx.car.app.navigation.model.TravelEstimate!> getStepTravelEstimates();
+    method public java.util.List<androidx.car.app.navigation.model.Step!> getSteps();
+    method public boolean isLoading();
+  }
+
+  public static final class Trip.Builder {
+    ctor public Trip.Builder();
+    method public androidx.car.app.navigation.model.Trip.Builder addDestination(androidx.car.app.navigation.model.Destination);
+    method public androidx.car.app.navigation.model.Trip.Builder addDestinationTravelEstimate(androidx.car.app.navigation.model.TravelEstimate);
+    method public androidx.car.app.navigation.model.Trip.Builder addStep(androidx.car.app.navigation.model.Step?);
+    method public androidx.car.app.navigation.model.Trip.Builder addStepTravelEstimate(androidx.car.app.navigation.model.TravelEstimate);
+    method public androidx.car.app.navigation.model.Trip build();
+    method public androidx.car.app.navigation.model.Trip.Builder clearDestinationTravelEstimates();
+    method public androidx.car.app.navigation.model.Trip.Builder clearDestinations();
+    method public androidx.car.app.navigation.model.Trip.Builder clearStepTravelEstimates();
+    method public androidx.car.app.navigation.model.Trip.Builder clearSteps();
+    method public androidx.car.app.navigation.model.Trip.Builder setCurrentRoad(CharSequence?);
+    method public androidx.car.app.navigation.model.Trip.Builder setIsLoading(boolean);
+  }
+
+}
+
+package androidx.car.app.notification {
+
+  public class CarAppExtender implements androidx.core.app.NotificationCompat.Extender {
+    ctor public CarAppExtender(android.app.Notification);
+    method public static androidx.car.app.notification.CarAppExtender.Builder builder();
+    method public androidx.core.app.NotificationCompat.Builder extend(androidx.core.app.NotificationCompat.Builder);
+    method public java.util.List<android.app.Notification.Action!> getActions();
+    method public android.app.PendingIntent? getContentIntent();
+    method public CharSequence? getContentText();
+    method public CharSequence? getContentTitle();
+    method public android.app.PendingIntent? getDeleteIntent();
+    method public int getImportance();
+    method public android.graphics.Bitmap? getLargeIconBitmap();
+    method public int getSmallIconResId();
+    method public boolean isExtended();
+    method public static boolean isExtended(android.app.Notification);
+  }
+
+  public static final class CarAppExtender.Builder {
+    ctor public CarAppExtender.Builder();
+    method public androidx.car.app.notification.CarAppExtender.Builder addAction(@DrawableRes int, CharSequence, android.app.PendingIntent);
+    method public androidx.car.app.notification.CarAppExtender build();
+    method public androidx.car.app.notification.CarAppExtender.Builder clearActions();
+    method public androidx.car.app.notification.CarAppExtender.Builder setContentIntent(android.app.PendingIntent?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setContentText(CharSequence?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setContentTitle(CharSequence?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setDeleteIntent(android.app.PendingIntent?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setImportance(int);
+    method public androidx.car.app.notification.CarAppExtender.Builder setLargeIcon(android.graphics.Bitmap?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setSmallIcon(int);
+  }
+
+}
+
 package androidx.car.app.serialization {
 
   public final class Bundleable implements android.os.Parcelable {
@@ -171,6 +1085,10 @@
 
 package androidx.car.app.utils {
 
+  public interface Logger {
+    method public void log(String);
+  }
+
   public class ThreadUtils {
     method public static void checkMainThread();
     method public static void runOnMain(Runnable);
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 8839e7a..a9eecc8 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -111,6 +111,7 @@
     method public final androidx.lifecycle.Lifecycle getLifecycle();
     method public String? getMarker();
     method public final androidx.car.app.ScreenManager getScreenManager();
+    method public abstract androidx.car.app.model.Template getTemplate();
     method public final void invalidate();
     method public void setMarker(String?);
     method public void setResult(Object?);
@@ -152,6 +153,919 @@
 
 }
 
+package androidx.car.app.model {
+
+  public final class Action {
+    method public static androidx.car.app.model.Action.Builder builder();
+    method public androidx.car.app.model.CarColor getBackgroundColor();
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public int getType();
+    method public boolean isStandard();
+    method public androidx.car.app.model.Action.Builder newBuilder();
+    method public static String typeToString(int);
+    field public static final androidx.car.app.model.Action APP_ICON;
+    field public static final androidx.car.app.model.Action BACK;
+    field public static final int TYPE_APP_ICON = 65538; // 0x10002
+    field public static final int TYPE_BACK = 65539; // 0x10003
+    field public static final int TYPE_CUSTOM = 1; // 0x1
+    field public static final int TYPE_UNKNOWN = 0; // 0x0
+  }
+
+  public static final class Action.Builder {
+    method public androidx.car.app.model.Action build();
+    method public androidx.car.app.model.Action.Builder setBackgroundColor(androidx.car.app.model.CarColor);
+    method public androidx.car.app.model.Action.Builder setIcon(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.model.Action.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+    method public androidx.car.app.model.Action.Builder setTitle(CharSequence?);
+  }
+
+  public class ActionList {
+    method public static androidx.car.app.model.ActionList create(java.util.List<androidx.car.app.model.Action!>);
+    method public java.util.List<androidx.car.app.model.Action!> getList();
+  }
+
+  public class ActionStrip {
+    method public static androidx.car.app.model.ActionStrip.Builder builder();
+    method public androidx.car.app.model.Action? getActionOfType(int);
+    method public java.util.List<java.lang.Object!> getActions();
+  }
+
+  public static final class ActionStrip.Builder {
+    ctor public ActionStrip.Builder();
+    method public androidx.car.app.model.ActionStrip.Builder addAction(androidx.car.app.model.Action);
+    method public androidx.car.app.model.ActionStrip build();
+    method public androidx.car.app.model.ActionStrip.Builder clearActions();
+  }
+
+  public class CarColor {
+    method public static androidx.car.app.model.CarColor createCustom(@ColorInt int, @ColorInt int);
+    method @ColorInt public int getColor();
+    method @ColorInt public int getColorDark();
+    method public int getType();
+    field public static final androidx.car.app.model.CarColor BLUE;
+    field public static final androidx.car.app.model.CarColor DEFAULT;
+    field public static final androidx.car.app.model.CarColor GREEN;
+    field public static final androidx.car.app.model.CarColor PRIMARY;
+    field public static final androidx.car.app.model.CarColor RED;
+    field public static final androidx.car.app.model.CarColor SECONDARY;
+    field public static final int TYPE_BLUE = 6; // 0x6
+    field public static final int TYPE_CUSTOM = 0; // 0x0
+    field public static final int TYPE_DEFAULT = 1; // 0x1
+    field public static final int TYPE_GREEN = 5; // 0x5
+    field public static final int TYPE_PRIMARY = 2; // 0x2
+    field public static final int TYPE_RED = 4; // 0x4
+    field public static final int TYPE_SECONDARY = 3; // 0x3
+    field public static final int TYPE_YELLOW = 7; // 0x7
+    field public static final androidx.car.app.model.CarColor YELLOW;
+  }
+
+  public class CarIcon {
+    method public static androidx.car.app.model.CarIcon.Builder builder(androidx.core.graphics.drawable.IconCompat);
+    method public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method public androidx.car.app.model.CarColor? getTint();
+    method public int getType();
+    method public androidx.car.app.model.CarIcon.Builder newBuilder();
+    method public static androidx.car.app.model.CarIcon of(androidx.core.graphics.drawable.IconCompat);
+    field public static final androidx.car.app.model.CarIcon ALERT;
+    field public static final androidx.car.app.model.CarIcon APP_ICON;
+    field public static final androidx.car.app.model.CarIcon BACK;
+    field public static final androidx.car.app.model.CarIcon ERROR;
+    field public static final int TYPE_ALERT = 4; // 0x4
+    field public static final int TYPE_APP = 5; // 0x5
+    field public static final int TYPE_BACK = 3; // 0x3
+    field public static final int TYPE_CUSTOM = 1; // 0x1
+    field public static final int TYPE_ERROR = 6; // 0x6
+    field public static final int TYPE_UNKNOWN = 0; // 0x0
+    field public static final int TYPE_WILLIAM_ALERT = 7; // 0x7
+    field public static final androidx.car.app.model.CarIcon WILLIAM_ALERT;
+  }
+
+  public static final class CarIcon.Builder {
+    method public androidx.car.app.model.CarIcon build();
+    method public androidx.car.app.model.CarIcon.Builder setIcon(androidx.car.app.model.CarIcon);
+    method public androidx.car.app.model.CarIcon.Builder setTint(androidx.car.app.model.CarColor?);
+  }
+
+  public class CarIconSpan extends android.text.style.CharacterStyle {
+    method public static androidx.car.app.model.CarIconSpan create(androidx.car.app.model.CarIcon);
+    method public static androidx.car.app.model.CarIconSpan create(androidx.car.app.model.CarIcon, int);
+    method public int getAlignment();
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public void updateDrawState(android.text.TextPaint?);
+    method public static int validateAlignment(int);
+    field public static final int ALIGN_BASELINE = 1; // 0x1
+    field public static final int ALIGN_BOTTOM = 0; // 0x0
+    field public static final int ALIGN_CENTER = 2; // 0x2
+  }
+
+  public class CarText {
+    ctor public CarText();
+    method public static androidx.car.app.model.CarText create(CharSequence);
+    method public java.util.List<androidx.car.app.model.CarText.SpanWrapper!> getSpans();
+    method public String? getText();
+    method public boolean isEmpty();
+    method public static boolean isNullOrEmpty(androidx.car.app.model.CarText?);
+    method public static String? toShortString(androidx.car.app.model.CarText?);
+    field public static final androidx.car.app.model.CarText EMPTY;
+  }
+
+  public static class CarText.SpanWrapper {
+    field @Keep public final int end;
+    field @Keep public final int flags;
+    field @Keep public final Object? span;
+    field @Keep public final int start;
+  }
+
+  public class DateTimeWithZone {
+    method public static androidx.car.app.model.DateTimeWithZone create(long, int, String);
+    method public static androidx.car.app.model.DateTimeWithZone create(long, java.util.TimeZone);
+    method @RequiresApi(26) public static androidx.car.app.model.DateTimeWithZone create(java.time.ZonedDateTime);
+    method public long getTimeSinceEpochMillis();
+    method public int getZoneOffsetSeconds();
+    method public String? getZoneShortName();
+  }
+
+  public final class Distance {
+    method public static androidx.car.app.model.Distance create(double, int);
+    method public double getDisplayDistance();
+    method public int getDisplayUnit();
+    field public static final int UNIT_FEET = 6; // 0x6
+    field public static final int UNIT_KILOMETERS = 2; // 0x2
+    field public static final int UNIT_KILOMETERS_P1 = 3; // 0x3
+    field public static final int UNIT_METERS = 1; // 0x1
+    field public static final int UNIT_MILES = 4; // 0x4
+    field public static final int UNIT_MILES_P1 = 5; // 0x5
+    field public static final int UNIT_YARDS = 7; // 0x7
+  }
+
+  public class DistanceSpan extends android.text.style.CharacterStyle {
+    method public static androidx.car.app.model.DistanceSpan create(androidx.car.app.model.Distance);
+    method public androidx.car.app.model.Distance getDistance();
+    method public void updateDrawState(android.text.TextPaint?);
+  }
+
+  public class DurationSpan extends android.text.style.CharacterStyle {
+    method public static androidx.car.app.model.DurationSpan create(long);
+    method @RequiresApi(26) public static androidx.car.app.model.DurationSpan create(java.time.Duration);
+    method public long getDurationSeconds();
+    method public void updateDrawState(android.text.TextPaint?);
+  }
+
+  public class ForegroundCarColorSpan extends android.text.style.CharacterStyle {
+    method public static androidx.car.app.model.ForegroundCarColorSpan create(androidx.car.app.model.CarColor);
+    method public androidx.car.app.model.CarColor getColor();
+    method public void updateDrawState(android.text.TextPaint);
+  }
+
+  public class GridItem implements androidx.car.app.model.Item {
+    method public static androidx.car.app.model.GridItem.Builder builder();
+    method public androidx.car.app.model.CarIcon getImage();
+    method public int getImageType();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.CarText? getText();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public androidx.car.app.model.Toggle? getToggle();
+    field public static final int IMAGE_TYPE_ICON = 1; // 0x1
+    field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
+  }
+
+  public static final class GridItem.Builder {
+    method public androidx.car.app.model.GridItem build();
+    method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon);
+    method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
+    method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+    method public androidx.car.app.model.GridItem.Builder setText(CharSequence?);
+    method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
+    method public androidx.car.app.model.GridItem.Builder setToggle(androidx.car.app.model.Toggle?);
+  }
+
+  public final class GridTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.GridTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.CarIcon? getBackgroundImage();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.ItemList? getSingleList();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isLoading();
+  }
+
+  public static final class GridTemplate.Builder {
+    method public androidx.car.app.model.GridTemplate build();
+    method public androidx.car.app.model.GridTemplate.Builder clearAllLists();
+    method public androidx.car.app.model.GridTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.GridTemplate.Builder setBackgroundImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.model.GridTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.GridTemplate.Builder setLoading(boolean);
+    method public androidx.car.app.model.GridTemplate.Builder setSingleList(androidx.car.app.model.ItemList);
+    method public androidx.car.app.model.GridTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public interface Item {
+  }
+
+  public final class ItemList {
+    method public static androidx.car.app.model.ItemList.Builder builder();
+    method public java.util.List<java.lang.Object!> getItems();
+    method public androidx.car.app.model.CarText? getNoItemsMessage();
+    method public int getSelectedIndex();
+    method public boolean isRefresh(androidx.car.app.model.ItemList?, androidx.car.app.utils.Logger);
+  }
+
+  public static final class ItemList.Builder {
+    ctor public ItemList.Builder();
+    method public androidx.car.app.model.ItemList.Builder addItem(androidx.car.app.model.Item);
+    method public androidx.car.app.model.ItemList build();
+    method public androidx.car.app.model.ItemList.Builder clearItems();
+    method public androidx.car.app.model.ItemList.Builder setNoItemsMessage(CharSequence?);
+    method public androidx.car.app.model.ItemList.Builder setOnItemsVisibilityChangeListener(androidx.car.app.model.ItemList.OnItemVisibilityChangedListener?);
+    method public androidx.car.app.model.ItemList.Builder setSelectable(androidx.car.app.model.ItemList.OnSelectedListener?);
+    method public androidx.car.app.model.ItemList.Builder setSelectedIndex(int);
+  }
+
+  public static interface ItemList.OnItemVisibilityChangedListener {
+    method public void onItemVisibilityChanged(int, int);
+  }
+
+  public static interface ItemList.OnSelectedListener {
+    method public void onSelected(int);
+  }
+
+  public final class LatLng {
+    method public static androidx.car.app.model.LatLng create(double, double);
+    method public static androidx.car.app.model.LatLng create(android.location.Location);
+    method public double getLatitude();
+    method public double getLongitude();
+  }
+
+  public final class ListTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.ListTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public java.util.List<androidx.car.app.model.SectionedItemList!> getSectionLists();
+    method public androidx.car.app.model.ItemList? getSingleList();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isLoading();
+  }
+
+  public static final class ListTemplate.Builder {
+    method public androidx.car.app.model.ListTemplate.Builder addList(androidx.car.app.model.ItemList, CharSequence);
+    method public androidx.car.app.model.ListTemplate build();
+    method public androidx.car.app.model.ListTemplate.Builder clearAllLists();
+    method public androidx.car.app.model.ListTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.ListTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.ListTemplate.Builder setLoading(boolean);
+    method public androidx.car.app.model.ListTemplate.Builder setSingleList(androidx.car.app.model.ItemList);
+    method public androidx.car.app.model.ListTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public final class MessageTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.MessageTemplate.Builder builder(CharSequence);
+    method public androidx.car.app.model.ActionList? getActionList();
+    method public androidx.car.app.model.CarText? getDebugMessage();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public androidx.car.app.model.CarText getMessage();
+    method public androidx.car.app.model.CarText? getTitle();
+  }
+
+  public static final class MessageTemplate.Builder {
+    method public androidx.car.app.model.MessageTemplate build();
+    method public androidx.car.app.model.MessageTemplate.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+    method public androidx.car.app.model.MessageTemplate.Builder setDebugCause(Throwable?);
+    method public androidx.car.app.model.MessageTemplate.Builder setDebugMessage(String?);
+    method public androidx.car.app.model.MessageTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.MessageTemplate.Builder setIcon(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.model.MessageTemplate.Builder setMessage(CharSequence);
+    method public androidx.car.app.model.MessageTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public class Metadata {
+    method public static androidx.car.app.model.Metadata.Builder builder();
+    method public androidx.car.app.model.Place? getPlace();
+    method public androidx.car.app.model.Metadata.Builder newBuilder();
+    method public static androidx.car.app.model.Metadata ofPlace(androidx.car.app.model.Place);
+    field public static final androidx.car.app.model.Metadata EMPTY_METADATA;
+  }
+
+  public static final class Metadata.Builder {
+    method public androidx.car.app.model.Metadata build();
+    method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
+  }
+
+  public interface OnClickListener {
+    method public void onClick();
+  }
+
+  public class OnClickListenerWrapper {
+    method public boolean isParkedOnly();
+  }
+
+  public final class Pane {
+    method public static androidx.car.app.model.Pane.Builder builder();
+    method public androidx.car.app.model.ActionList? getActionList();
+    method public java.util.List<java.lang.Object!> getRows();
+    method public boolean isLoading();
+    method public boolean isRefresh(androidx.car.app.model.Pane?, androidx.car.app.utils.Logger);
+  }
+
+  public static final class Pane.Builder {
+    ctor public Pane.Builder();
+    method public androidx.car.app.model.Pane.Builder addRow(androidx.car.app.model.Row);
+    method public androidx.car.app.model.Pane build();
+    method public androidx.car.app.model.Pane.Builder clearRows();
+    method public androidx.car.app.model.Pane.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+    method public androidx.car.app.model.Pane.Builder setLoading(boolean);
+  }
+
+  public final class PaneTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.PaneTemplate.Builder builder(androidx.car.app.model.Pane);
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.Pane getPane();
+    method public androidx.car.app.model.CarText? getTitle();
+  }
+
+  public static final class PaneTemplate.Builder {
+    method public androidx.car.app.model.PaneTemplate build();
+    method public androidx.car.app.model.PaneTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.PaneTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.PaneTemplate.Builder setPane(androidx.car.app.model.Pane);
+    method public androidx.car.app.model.PaneTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public final class ParkedOnlyOnClickListener implements androidx.car.app.model.OnClickListener {
+    method public static androidx.car.app.model.ParkedOnlyOnClickListener create(androidx.car.app.model.OnClickListener);
+    method public void onClick();
+  }
+
+  public class Place {
+    method public static androidx.car.app.model.Place.Builder builder(androidx.car.app.model.LatLng);
+    method public androidx.car.app.model.LatLng getLatLng();
+    method public androidx.car.app.model.PlaceMarker? getMarker();
+    method public androidx.car.app.model.Place.Builder newBuilder();
+  }
+
+  public static final class Place.Builder {
+    method public androidx.car.app.model.Place build();
+    method public androidx.car.app.model.Place.Builder setLatLng(androidx.car.app.model.LatLng);
+    method public androidx.car.app.model.Place.Builder setMarker(androidx.car.app.model.PlaceMarker?);
+  }
+
+  public final class PlaceListMapTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.PlaceListMapTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Place? getAnchor();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.ItemList? getItemList();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isCurrentLocationEnabled();
+    method public boolean isLoading();
+  }
+
+  public static final class PlaceListMapTemplate.Builder {
+    ctor public PlaceListMapTemplate.Builder();
+    method public androidx.car.app.model.PlaceListMapTemplate build();
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setAnchor(androidx.car.app.model.Place?);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setCurrentLocationEnabled(boolean);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setLoading(boolean);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public class PlaceMarker {
+    method public static androidx.car.app.model.PlaceMarker.Builder builder();
+    method public androidx.car.app.model.CarColor? getColor();
+    method public static androidx.car.app.model.PlaceMarker getDefault();
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public int getIconType();
+    method public androidx.car.app.model.CarText? getLabel();
+    method public static boolean isDefaultMarker(androidx.car.app.model.PlaceMarker?);
+    field public static final int TYPE_ICON = 0; // 0x0
+    field public static final int TYPE_IMAGE = 1; // 0x1
+  }
+
+  public static final class PlaceMarker.Builder {
+    method public androidx.car.app.model.PlaceMarker build();
+    method public androidx.car.app.model.PlaceMarker.Builder setColor(androidx.car.app.model.CarColor?);
+    method public androidx.car.app.model.PlaceMarker.Builder setIcon(androidx.car.app.model.CarIcon?, int);
+    method public androidx.car.app.model.PlaceMarker.Builder setLabel(CharSequence?);
+  }
+
+  public class Row implements androidx.car.app.model.Item {
+    method public static androidx.car.app.model.Row.Builder builder();
+    method public int getFlags();
+    method public androidx.car.app.model.CarIcon? getImage();
+    method public androidx.car.app.model.Metadata getMetadata();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+    method public int getRowImageType();
+    method public java.util.List<androidx.car.app.model.CarText!> getTexts();
+    method public androidx.car.app.model.CarText getTitle();
+    method public androidx.car.app.model.Toggle? getToggle();
+    method public boolean isBrowsable();
+    method public androidx.car.app.model.Row row();
+    method public void yourBoat();
+    field public static final int IMAGE_TYPE_ICON = 4; // 0x4
+    field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
+    field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
+    field public static final int ROW_FLAG_NONE = 1; // 0x1
+    field public static final int ROW_FLAG_SECTION_HEADER = 4; // 0x4
+    field public static final int ROW_FLAG_SHOW_DIVIDERS = 2; // 0x2
+  }
+
+  public static final class Row.Builder {
+    method public androidx.car.app.model.Row.Builder addText(CharSequence);
+    method public androidx.car.app.model.Row build();
+    method public androidx.car.app.model.Row.Builder clearText();
+    method public androidx.car.app.model.Row.Builder setBrowsable(boolean);
+    method public androidx.car.app.model.Row.Builder setFlags(int);
+    method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?, int);
+    method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
+    method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+    method public androidx.car.app.model.Row.Builder setTitle(CharSequence);
+    method public androidx.car.app.model.Row.Builder setToggle(androidx.car.app.model.Toggle?);
+  }
+
+  public final class SearchTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.SearchListener);
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public String? getInitialSearchText();
+    method public androidx.car.app.model.ItemList? getItemList();
+    method public String? getSearchHint();
+    method public boolean isLoading();
+    method public boolean isShowKeyboardByDefault();
+  }
+
+  public static final class SearchTemplate.Builder {
+    method public androidx.car.app.model.SearchTemplate build();
+    method public androidx.car.app.model.SearchTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.SearchTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.SearchTemplate.Builder setInitialSearchText(String?);
+    method public androidx.car.app.model.SearchTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.model.SearchTemplate.Builder setLoading(boolean);
+    method public androidx.car.app.model.SearchTemplate.Builder setSearchHint(String?);
+    method public androidx.car.app.model.SearchTemplate.Builder setShowKeyboardByDefault(boolean);
+  }
+
+  public class SectionedItemList {
+    method public static androidx.car.app.model.SectionedItemList create(androidx.car.app.model.ItemList, androidx.car.app.model.CarText);
+    method public androidx.car.app.model.CarText getHeader();
+    method public androidx.car.app.model.ItemList getItemList();
+  }
+
+  public interface Template {
+    method public default void checkPermissions(android.content.Context);
+    method public default boolean isRefresh(androidx.car.app.model.Template, androidx.car.app.utils.Logger);
+  }
+
+  public final class TemplateInfo {
+    ctor public TemplateInfo(androidx.car.app.model.Template, String);
+    method public Class<? extends androidx.car.app.model.Template> getTemplateClass();
+    method public String getTemplateId();
+  }
+
+  public final class TemplateWrapper {
+    method public static androidx.car.app.model.TemplateWrapper copyOf(androidx.car.app.model.TemplateWrapper);
+    method public int getCurrentTaskStep();
+    method public String getId();
+    method public androidx.car.app.model.Template getTemplate();
+    method public java.util.List<androidx.car.app.model.TemplateInfo!>? getTemplateInfosForScreenStack();
+    method public boolean isRefresh();
+    method public void setCurrentTaskStep(int);
+    method public void setId(String);
+    method public void setRefresh(boolean);
+    method public void setTemplate(androidx.car.app.model.Template);
+    method public static androidx.car.app.model.TemplateWrapper wrap(androidx.car.app.model.Template);
+    method public static androidx.car.app.model.TemplateWrapper wrap(androidx.car.app.model.Template, String);
+  }
+
+  public class Toggle {
+    method public static androidx.car.app.model.Toggle.Builder builder(androidx.car.app.model.Toggle.OnCheckedChangeListener);
+    method public boolean isChecked();
+  }
+
+  public static final class Toggle.Builder {
+    method public androidx.car.app.model.Toggle build();
+    method public androidx.car.app.model.Toggle.Builder setChecked(boolean);
+    method public androidx.car.app.model.Toggle.Builder setCheckedChangeListener(androidx.car.app.model.Toggle.OnCheckedChangeListener);
+  }
+
+  public static interface Toggle.OnCheckedChangeListener {
+    method public void onCheckedChange(boolean);
+  }
+
+}
+
+package androidx.car.app.model.constraints {
+
+  public class ActionsConstraints {
+    method @VisibleForTesting public static androidx.car.app.model.constraints.ActionsConstraints.Builder builder();
+    method public java.util.Set<java.lang.Integer!> getDisallowedActionTypes();
+    method public int getMaxActions();
+    method public int getMaxCustomTitles();
+    method public java.util.Set<java.lang.Integer!> getRequiredActionTypes();
+    method @VisibleForTesting public androidx.car.app.model.constraints.ActionsConstraints.Builder newBuilder();
+    method public void validateOrThrow(java.util.List<java.lang.Object!>);
+    field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_HEADER;
+    field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION;
+    field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_SIMPLE;
+  }
+
+  @VisibleForTesting public static final class ActionsConstraints.Builder {
+    method public androidx.car.app.model.constraints.ActionsConstraints.Builder addDisallowedActionType(int);
+    method public androidx.car.app.model.constraints.ActionsConstraints.Builder addRequiredActionType(int);
+    method public androidx.car.app.model.constraints.ActionsConstraints build();
+    method public androidx.car.app.model.constraints.ActionsConstraints.Builder setMaxActions(int);
+    method public androidx.car.app.model.constraints.ActionsConstraints.Builder setMaxCustomTitles(int);
+  }
+
+  public class CarColorConstraints {
+    method public void validateOrThrow(androidx.car.app.model.CarColor);
+    field public static final androidx.car.app.model.constraints.CarColorConstraints STANDARD_ONLY;
+    field public static final androidx.car.app.model.constraints.CarColorConstraints UNCONSTRAINED;
+  }
+
+  public class CarIconConstraints {
+    method public androidx.core.graphics.drawable.IconCompat checkSupportedIcon(androidx.core.graphics.drawable.IconCompat);
+    method public void validateOrThrow(androidx.car.app.model.CarIcon?);
+    field public static final androidx.car.app.model.constraints.CarIconConstraints DEFAULT;
+    field public static final androidx.car.app.model.constraints.CarIconConstraints UNCONSTRAINED;
+  }
+
+  public class RowConstraints {
+    method public static androidx.car.app.model.constraints.RowConstraints.Builder builder();
+    method public androidx.car.app.model.constraints.CarIconConstraints getCarIconConstraints();
+    method public int getFlagOverrides();
+    method public int getMaxActionsExclusive();
+    method public int getMaxTextLinesPerRow();
+    method public boolean isImageAllowed();
+    method public boolean isOnClickListenerAllowed();
+    method public boolean isToggleAllowed();
+    method public androidx.car.app.model.constraints.RowConstraints.Builder newBuilder();
+    method public void validateOrThrow(Object);
+    field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_CONSERVATIVE;
+    field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_FULL_LIST;
+    field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_PANE;
+    field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_SIMPLE;
+    field public static final androidx.car.app.model.constraints.RowConstraints UNCONSTRAINED;
+  }
+
+  public static final class RowConstraints.Builder {
+    method public androidx.car.app.model.constraints.RowConstraints build();
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setCarIconConstraints(androidx.car.app.model.constraints.CarIconConstraints);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setFlagOverrides(int);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setImageAllowed(boolean);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxActionsExclusive(int);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxTextLinesPerRow(int);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setOnClickListenerAllowed(boolean);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setToggleAllowed(boolean);
+  }
+
+  public class RowListConstraints {
+    method public static androidx.car.app.model.constraints.RowListConstraints.Builder builder();
+    method public int getMaxActions();
+    method public androidx.car.app.model.constraints.RowConstraints getRowConstraints();
+    method public int getRowListType();
+    method public boolean isAllowSelectableLists();
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder newBuilder();
+    method public void validateOrThrow(androidx.car.app.model.ItemList);
+    method public void validateOrThrow(java.util.List<androidx.car.app.model.SectionedItemList!>);
+    method public void validateOrThrow(androidx.car.app.model.Pane);
+    field public static final int DEFAULT_LIST = 0; // 0x0
+    field public static final int PANE = 1; // 0x1
+    field public static final int ROUTE_PREVIEW = 2; // 0x2
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_CONSERVATIVE;
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_FULL_LIST;
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_PANE;
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW;
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_SIMPLE;
+  }
+
+  public static final class RowListConstraints.Builder {
+    method public androidx.car.app.model.constraints.RowListConstraints build();
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder setAllowSelectableLists(boolean);
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder setMaxActions(int);
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder setRowConstraints(androidx.car.app.model.constraints.RowConstraints);
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder setRowListType(int);
+  }
+
+}
+
+package androidx.car.app.navigation {
+
+  public class NavigationManager {
+    method @MainThread public void navigationEnded();
+    method @MainThread public void navigationStarted();
+    method @MainThread public void setListener(androidx.car.app.navigation.NavigationManagerListener?);
+    method @MainThread public void updateTrip(androidx.car.app.navigation.model.Trip);
+  }
+
+  public interface NavigationManagerListener {
+    method public void onAutoDriveEnabled();
+    method public void stopNavigation();
+  }
+
+}
+
+package androidx.car.app.navigation.model {
+
+  public final class Destination {
+    method public static androidx.car.app.navigation.model.Destination.Builder builder(CharSequence, CharSequence);
+    method public static androidx.car.app.navigation.model.Destination.Builder builder();
+    method public androidx.car.app.model.CarText? getAddress();
+    method public androidx.car.app.model.CarIcon? getImage();
+    method public androidx.car.app.model.CarText? getName();
+  }
+
+  public static final class Destination.Builder {
+    method public androidx.car.app.navigation.model.Destination build();
+    method public androidx.car.app.navigation.model.Destination.Builder setAddress(CharSequence?);
+    method public androidx.car.app.navigation.model.Destination.Builder setImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.Destination.Builder setName(CharSequence?);
+  }
+
+  public final class Lane {
+    method public static androidx.car.app.navigation.model.Lane.Builder builder();
+    method public java.util.List<androidx.car.app.navigation.model.LaneDirection!> getDirections();
+  }
+
+  public static final class Lane.Builder {
+    ctor public Lane.Builder();
+    method public androidx.car.app.navigation.model.Lane.Builder addDirection(androidx.car.app.navigation.model.LaneDirection);
+    method public androidx.car.app.navigation.model.Lane build();
+    method public androidx.car.app.navigation.model.Lane.Builder clearDirections();
+  }
+
+  public final class LaneDirection {
+    method public static androidx.car.app.navigation.model.LaneDirection create(int, boolean);
+    method public int getShape();
+    method public boolean isHighlighted();
+    field public static final int SHAPE_NORMAL_LEFT = 5; // 0x5
+    field public static final int SHAPE_NORMAL_RIGHT = 6; // 0x6
+    field public static final int SHAPE_SHARP_LEFT = 7; // 0x7
+    field public static final int SHAPE_SHARP_RIGHT = 8; // 0x8
+    field public static final int SHAPE_SLIGHT_LEFT = 3; // 0x3
+    field public static final int SHAPE_SLIGHT_RIGHT = 4; // 0x4
+    field public static final int SHAPE_STRAIGHT = 2; // 0x2
+    field public static final int SHAPE_UNKNOWN = 1; // 0x1
+    field public static final int SHAPE_U_TURN_LEFT = 9; // 0x9
+    field public static final int SHAPE_U_TURN_RIGHT = 10; // 0xa
+  }
+
+  public final class Maneuver {
+    method public static androidx.car.app.navigation.model.Maneuver.Builder builder(int);
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public int getRoundaboutExitAngle();
+    method public int getRoundaboutExitNumber();
+    method public int getType();
+    field public static final int TYPE_DEPART = 1; // 0x1
+    field public static final int TYPE_DESTINATION = 39; // 0x27
+    field public static final int TYPE_DESTINATION_LEFT = 41; // 0x29
+    field public static final int TYPE_DESTINATION_RIGHT = 42; // 0x2a
+    field public static final int TYPE_DESTINATION_STRAIGHT = 40; // 0x28
+    field public static final int TYPE_FERRY_BOAT = 37; // 0x25
+    field public static final int TYPE_FERRY_TRAIN = 38; // 0x26
+    field public static final int TYPE_FORK_LEFT = 25; // 0x19
+    field public static final int TYPE_FORK_RIGHT = 26; // 0x1a
+    field public static final int TYPE_KEEP_LEFT = 3; // 0x3
+    field public static final int TYPE_KEEP_RIGHT = 4; // 0x4
+    field public static final int TYPE_MERGE_LEFT = 27; // 0x1b
+    field public static final int TYPE_MERGE_RIGHT = 28; // 0x1c
+    field public static final int TYPE_MERGE_SIDE_UNSPECIFIED = 29; // 0x1d
+    field public static final int TYPE_NAME_CHANGE = 2; // 0x2
+    field public static final int TYPE_OFF_RAMP_NORMAL_LEFT = 23; // 0x17
+    field public static final int TYPE_OFF_RAMP_NORMAL_RIGHT = 24; // 0x18
+    field public static final int TYPE_OFF_RAMP_SLIGHT_LEFT = 21; // 0x15
+    field public static final int TYPE_OFF_RAMP_SLIGHT_RIGHT = 22; // 0x16
+    field public static final int TYPE_ON_RAMP_NORMAL_LEFT = 15; // 0xf
+    field public static final int TYPE_ON_RAMP_NORMAL_RIGHT = 16; // 0x10
+    field public static final int TYPE_ON_RAMP_SHARP_LEFT = 17; // 0x11
+    field public static final int TYPE_ON_RAMP_SHARP_RIGHT = 18; // 0x12
+    field public static final int TYPE_ON_RAMP_SLIGHT_LEFT = 13; // 0xd
+    field public static final int TYPE_ON_RAMP_SLIGHT_RIGHT = 14; // 0xe
+    field public static final int TYPE_ON_RAMP_U_TURN_LEFT = 19; // 0x13
+    field public static final int TYPE_ON_RAMP_U_TURN_RIGHT = 20; // 0x14
+    field public static final int TYPE_ROUNDABOUT_ENTER = 30; // 0x1e
+    field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW = 34; // 0x22
+    field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE = 35; // 0x23
+    field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW = 32; // 0x20
+    field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE = 33; // 0x21
+    field public static final int TYPE_ROUNDABOUT_EXIT = 31; // 0x1f
+    field public static final int TYPE_STRAIGHT = 36; // 0x24
+    field public static final int TYPE_TURN_NORMAL_LEFT = 7; // 0x7
+    field public static final int TYPE_TURN_NORMAL_RIGHT = 8; // 0x8
+    field public static final int TYPE_TURN_SHARP_LEFT = 9; // 0x9
+    field public static final int TYPE_TURN_SHARP_RIGHT = 10; // 0xa
+    field public static final int TYPE_TURN_SLIGHT_LEFT = 5; // 0x5
+    field public static final int TYPE_TURN_SLIGHT_RIGHT = 6; // 0x6
+    field public static final int TYPE_UNKNOWN = 0; // 0x0
+    field public static final int TYPE_U_TURN_LEFT = 11; // 0xb
+    field public static final int TYPE_U_TURN_RIGHT = 12; // 0xc
+  }
+
+  public static final class Maneuver.Builder {
+    method public androidx.car.app.navigation.model.Maneuver build();
+    method public androidx.car.app.navigation.model.Maneuver.Builder setIcon(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.Maneuver.Builder setRoundaboutExitAngle(int);
+    method public androidx.car.app.navigation.model.Maneuver.Builder setRoundaboutExitNumber(int);
+  }
+
+  public class MessageInfo implements androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo {
+    method public static androidx.car.app.navigation.model.MessageInfo.Builder builder(CharSequence);
+    method public androidx.car.app.model.CarIcon? getImage();
+    method public androidx.car.app.model.CarText? getText();
+    method public androidx.car.app.model.CarText getTitle();
+  }
+
+  public static final class MessageInfo.Builder {
+    method public androidx.car.app.navigation.model.MessageInfo build();
+    method public androidx.car.app.navigation.model.MessageInfo.Builder setImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.MessageInfo.Builder setText(CharSequence?);
+    method public androidx.car.app.navigation.model.MessageInfo.Builder setTitle(CharSequence);
+  }
+
+  public class NavigationTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.navigation.model.NavigationTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip getActionStrip();
+    method public androidx.car.app.model.CarColor? getBackgroundColor();
+    method public androidx.car.app.navigation.model.TravelEstimate? getDestinationTravelEstimate();
+    method public androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo? getNavigationInfo();
+  }
+
+  public static final class NavigationTemplate.Builder {
+    method public androidx.car.app.navigation.model.NavigationTemplate build();
+    method public androidx.car.app.navigation.model.NavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip);
+    method public androidx.car.app.navigation.model.NavigationTemplate.Builder setBackgroundColor(androidx.car.app.model.CarColor?);
+    method public androidx.car.app.navigation.model.NavigationTemplate.Builder setDestinationTravelEstimate(androidx.car.app.navigation.model.TravelEstimate?);
+    method public androidx.car.app.navigation.model.NavigationTemplate.Builder setNavigationInfo(androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo?);
+  }
+
+  public static interface NavigationTemplate.NavigationInfo {
+  }
+
+  public final class PlaceListNavigationTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.ItemList? getItemList();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isLoading();
+  }
+
+  public static final class PlaceListNavigationTemplate.Builder {
+    ctor public PlaceListNavigationTemplate.Builder();
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate build();
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setIsLoading(boolean);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+    method @VisibleForTesting public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public final class RoutePreviewNavigationTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.ItemList? getItemList();
+    method public androidx.car.app.model.Action? getNavigateAction();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isLoading();
+  }
+
+  public static final class RoutePreviewNavigationTemplate.Builder {
+    ctor public RoutePreviewNavigationTemplate.Builder();
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate build();
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setIsLoading(boolean);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+    method @VisibleForTesting public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public class RoutingInfo implements androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo {
+    method public static androidx.car.app.navigation.model.RoutingInfo.Builder builder();
+    method public androidx.car.app.model.Distance? getCurrentDistance();
+    method public androidx.car.app.navigation.model.Step? getCurrentStep();
+    method public androidx.car.app.model.CarIcon? getJunctionImage();
+    method public androidx.car.app.navigation.model.Step? getNextStep();
+    method public boolean isLoading();
+  }
+
+  public static final class RoutingInfo.Builder {
+    method public androidx.car.app.navigation.model.RoutingInfo build();
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setCurrentStep(androidx.car.app.navigation.model.Step, androidx.car.app.model.Distance);
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setIsLoading(boolean);
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setJunctionImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setNextStep(androidx.car.app.navigation.model.Step?);
+  }
+
+  public final class Step {
+    method public static androidx.car.app.navigation.model.Step.Builder builder(CharSequence);
+    method public androidx.car.app.model.CarText? getCue();
+    method public java.util.List<androidx.car.app.navigation.model.Lane!> getLanes();
+    method public androidx.car.app.model.CarIcon? getLanesImage();
+    method public androidx.car.app.navigation.model.Maneuver? getManeuver();
+    method public androidx.car.app.model.CarText? getRoad();
+    method public androidx.car.app.navigation.model.Step.Builder newBuilder();
+  }
+
+  public static final class Step.Builder {
+    method public androidx.car.app.navigation.model.Step.Builder addLane(androidx.car.app.navigation.model.Lane);
+    method public androidx.car.app.navigation.model.Step build();
+    method public androidx.car.app.navigation.model.Step.Builder clearLanes();
+    method public androidx.car.app.navigation.model.Step.Builder setCue(CharSequence);
+    method public androidx.car.app.navigation.model.Step.Builder setLanesImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.Step.Builder setManeuver(androidx.car.app.navigation.model.Maneuver?);
+    method public androidx.car.app.navigation.model.Step.Builder setRoad(CharSequence);
+  }
+
+  public final class TravelEstimate {
+    method public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
+    method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+    method public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
+    method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+    method public androidx.car.app.model.DateTimeWithZone? getArrivalTimeAtDestination();
+    method public androidx.car.app.model.Distance getRemainingDistance();
+    method public androidx.car.app.model.CarColor getRemainingDistanceColor();
+    method public androidx.car.app.model.CarColor getRemainingTimeColor();
+    method public long getRemainingTimeSeconds();
+  }
+
+  public static final class TravelEstimate.Builder {
+    method public androidx.car.app.navigation.model.TravelEstimate build();
+    method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingDistanceColor(androidx.car.app.model.CarColor);
+    method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTimeColor(androidx.car.app.model.CarColor);
+  }
+
+  public final class Trip {
+    method public static androidx.car.app.navigation.model.Trip.Builder builder();
+    method public androidx.car.app.model.CarText? getCurrentRoad();
+    method public java.util.List<androidx.car.app.navigation.model.TravelEstimate!> getDestinationTravelEstimates();
+    method public java.util.List<androidx.car.app.navigation.model.Destination!> getDestinations();
+    method public java.util.List<androidx.car.app.navigation.model.TravelEstimate!> getStepTravelEstimates();
+    method public java.util.List<androidx.car.app.navigation.model.Step!> getSteps();
+    method public boolean isLoading();
+  }
+
+  public static final class Trip.Builder {
+    ctor public Trip.Builder();
+    method public androidx.car.app.navigation.model.Trip.Builder addDestination(androidx.car.app.navigation.model.Destination);
+    method public androidx.car.app.navigation.model.Trip.Builder addDestinationTravelEstimate(androidx.car.app.navigation.model.TravelEstimate);
+    method public androidx.car.app.navigation.model.Trip.Builder addStep(androidx.car.app.navigation.model.Step?);
+    method public androidx.car.app.navigation.model.Trip.Builder addStepTravelEstimate(androidx.car.app.navigation.model.TravelEstimate);
+    method public androidx.car.app.navigation.model.Trip build();
+    method public androidx.car.app.navigation.model.Trip.Builder clearDestinationTravelEstimates();
+    method public androidx.car.app.navigation.model.Trip.Builder clearDestinations();
+    method public androidx.car.app.navigation.model.Trip.Builder clearStepTravelEstimates();
+    method public androidx.car.app.navigation.model.Trip.Builder clearSteps();
+    method public androidx.car.app.navigation.model.Trip.Builder setCurrentRoad(CharSequence?);
+    method public androidx.car.app.navigation.model.Trip.Builder setIsLoading(boolean);
+  }
+
+}
+
+package androidx.car.app.notification {
+
+  public class CarAppExtender implements androidx.core.app.NotificationCompat.Extender {
+    ctor public CarAppExtender(android.app.Notification);
+    method public static androidx.car.app.notification.CarAppExtender.Builder builder();
+    method public androidx.core.app.NotificationCompat.Builder extend(androidx.core.app.NotificationCompat.Builder);
+    method public java.util.List<android.app.Notification.Action!> getActions();
+    method public android.app.PendingIntent? getContentIntent();
+    method public CharSequence? getContentText();
+    method public CharSequence? getContentTitle();
+    method public android.app.PendingIntent? getDeleteIntent();
+    method public int getImportance();
+    method public android.graphics.Bitmap? getLargeIconBitmap();
+    method public int getSmallIconResId();
+    method public boolean isExtended();
+    method public static boolean isExtended(android.app.Notification);
+  }
+
+  public static final class CarAppExtender.Builder {
+    ctor public CarAppExtender.Builder();
+    method public androidx.car.app.notification.CarAppExtender.Builder addAction(@DrawableRes int, CharSequence, android.app.PendingIntent);
+    method public androidx.car.app.notification.CarAppExtender build();
+    method public androidx.car.app.notification.CarAppExtender.Builder clearActions();
+    method public androidx.car.app.notification.CarAppExtender.Builder setContentIntent(android.app.PendingIntent?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setContentText(CharSequence?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setContentTitle(CharSequence?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setDeleteIntent(android.app.PendingIntent?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setImportance(int);
+    method public androidx.car.app.notification.CarAppExtender.Builder setLargeIcon(android.graphics.Bitmap?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setSmallIcon(int);
+  }
+
+}
+
 package androidx.car.app.serialization {
 
   public final class Bundleable implements android.os.Parcelable {
@@ -171,6 +1085,10 @@
 
 package androidx.car.app.utils {
 
+  public interface Logger {
+    method public void log(String);
+  }
+
   public class ThreadUtils {
     method public static void checkMainThread();
     method public static void runOnMain(Runnable);
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 8839e7a..a9eecc8 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -111,6 +111,7 @@
     method public final androidx.lifecycle.Lifecycle getLifecycle();
     method public String? getMarker();
     method public final androidx.car.app.ScreenManager getScreenManager();
+    method public abstract androidx.car.app.model.Template getTemplate();
     method public final void invalidate();
     method public void setMarker(String?);
     method public void setResult(Object?);
@@ -152,6 +153,919 @@
 
 }
 
+package androidx.car.app.model {
+
+  public final class Action {
+    method public static androidx.car.app.model.Action.Builder builder();
+    method public androidx.car.app.model.CarColor getBackgroundColor();
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public int getType();
+    method public boolean isStandard();
+    method public androidx.car.app.model.Action.Builder newBuilder();
+    method public static String typeToString(int);
+    field public static final androidx.car.app.model.Action APP_ICON;
+    field public static final androidx.car.app.model.Action BACK;
+    field public static final int TYPE_APP_ICON = 65538; // 0x10002
+    field public static final int TYPE_BACK = 65539; // 0x10003
+    field public static final int TYPE_CUSTOM = 1; // 0x1
+    field public static final int TYPE_UNKNOWN = 0; // 0x0
+  }
+
+  public static final class Action.Builder {
+    method public androidx.car.app.model.Action build();
+    method public androidx.car.app.model.Action.Builder setBackgroundColor(androidx.car.app.model.CarColor);
+    method public androidx.car.app.model.Action.Builder setIcon(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.model.Action.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+    method public androidx.car.app.model.Action.Builder setTitle(CharSequence?);
+  }
+
+  public class ActionList {
+    method public static androidx.car.app.model.ActionList create(java.util.List<androidx.car.app.model.Action!>);
+    method public java.util.List<androidx.car.app.model.Action!> getList();
+  }
+
+  public class ActionStrip {
+    method public static androidx.car.app.model.ActionStrip.Builder builder();
+    method public androidx.car.app.model.Action? getActionOfType(int);
+    method public java.util.List<java.lang.Object!> getActions();
+  }
+
+  public static final class ActionStrip.Builder {
+    ctor public ActionStrip.Builder();
+    method public androidx.car.app.model.ActionStrip.Builder addAction(androidx.car.app.model.Action);
+    method public androidx.car.app.model.ActionStrip build();
+    method public androidx.car.app.model.ActionStrip.Builder clearActions();
+  }
+
+  public class CarColor {
+    method public static androidx.car.app.model.CarColor createCustom(@ColorInt int, @ColorInt int);
+    method @ColorInt public int getColor();
+    method @ColorInt public int getColorDark();
+    method public int getType();
+    field public static final androidx.car.app.model.CarColor BLUE;
+    field public static final androidx.car.app.model.CarColor DEFAULT;
+    field public static final androidx.car.app.model.CarColor GREEN;
+    field public static final androidx.car.app.model.CarColor PRIMARY;
+    field public static final androidx.car.app.model.CarColor RED;
+    field public static final androidx.car.app.model.CarColor SECONDARY;
+    field public static final int TYPE_BLUE = 6; // 0x6
+    field public static final int TYPE_CUSTOM = 0; // 0x0
+    field public static final int TYPE_DEFAULT = 1; // 0x1
+    field public static final int TYPE_GREEN = 5; // 0x5
+    field public static final int TYPE_PRIMARY = 2; // 0x2
+    field public static final int TYPE_RED = 4; // 0x4
+    field public static final int TYPE_SECONDARY = 3; // 0x3
+    field public static final int TYPE_YELLOW = 7; // 0x7
+    field public static final androidx.car.app.model.CarColor YELLOW;
+  }
+
+  public class CarIcon {
+    method public static androidx.car.app.model.CarIcon.Builder builder(androidx.core.graphics.drawable.IconCompat);
+    method public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method public androidx.car.app.model.CarColor? getTint();
+    method public int getType();
+    method public androidx.car.app.model.CarIcon.Builder newBuilder();
+    method public static androidx.car.app.model.CarIcon of(androidx.core.graphics.drawable.IconCompat);
+    field public static final androidx.car.app.model.CarIcon ALERT;
+    field public static final androidx.car.app.model.CarIcon APP_ICON;
+    field public static final androidx.car.app.model.CarIcon BACK;
+    field public static final androidx.car.app.model.CarIcon ERROR;
+    field public static final int TYPE_ALERT = 4; // 0x4
+    field public static final int TYPE_APP = 5; // 0x5
+    field public static final int TYPE_BACK = 3; // 0x3
+    field public static final int TYPE_CUSTOM = 1; // 0x1
+    field public static final int TYPE_ERROR = 6; // 0x6
+    field public static final int TYPE_UNKNOWN = 0; // 0x0
+    field public static final int TYPE_WILLIAM_ALERT = 7; // 0x7
+    field public static final androidx.car.app.model.CarIcon WILLIAM_ALERT;
+  }
+
+  public static final class CarIcon.Builder {
+    method public androidx.car.app.model.CarIcon build();
+    method public androidx.car.app.model.CarIcon.Builder setIcon(androidx.car.app.model.CarIcon);
+    method public androidx.car.app.model.CarIcon.Builder setTint(androidx.car.app.model.CarColor?);
+  }
+
+  public class CarIconSpan extends android.text.style.CharacterStyle {
+    method public static androidx.car.app.model.CarIconSpan create(androidx.car.app.model.CarIcon);
+    method public static androidx.car.app.model.CarIconSpan create(androidx.car.app.model.CarIcon, int);
+    method public int getAlignment();
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public void updateDrawState(android.text.TextPaint?);
+    method public static int validateAlignment(int);
+    field public static final int ALIGN_BASELINE = 1; // 0x1
+    field public static final int ALIGN_BOTTOM = 0; // 0x0
+    field public static final int ALIGN_CENTER = 2; // 0x2
+  }
+
+  public class CarText {
+    ctor public CarText();
+    method public static androidx.car.app.model.CarText create(CharSequence);
+    method public java.util.List<androidx.car.app.model.CarText.SpanWrapper!> getSpans();
+    method public String? getText();
+    method public boolean isEmpty();
+    method public static boolean isNullOrEmpty(androidx.car.app.model.CarText?);
+    method public static String? toShortString(androidx.car.app.model.CarText?);
+    field public static final androidx.car.app.model.CarText EMPTY;
+  }
+
+  public static class CarText.SpanWrapper {
+    field @Keep public final int end;
+    field @Keep public final int flags;
+    field @Keep public final Object? span;
+    field @Keep public final int start;
+  }
+
+  public class DateTimeWithZone {
+    method public static androidx.car.app.model.DateTimeWithZone create(long, int, String);
+    method public static androidx.car.app.model.DateTimeWithZone create(long, java.util.TimeZone);
+    method @RequiresApi(26) public static androidx.car.app.model.DateTimeWithZone create(java.time.ZonedDateTime);
+    method public long getTimeSinceEpochMillis();
+    method public int getZoneOffsetSeconds();
+    method public String? getZoneShortName();
+  }
+
+  public final class Distance {
+    method public static androidx.car.app.model.Distance create(double, int);
+    method public double getDisplayDistance();
+    method public int getDisplayUnit();
+    field public static final int UNIT_FEET = 6; // 0x6
+    field public static final int UNIT_KILOMETERS = 2; // 0x2
+    field public static final int UNIT_KILOMETERS_P1 = 3; // 0x3
+    field public static final int UNIT_METERS = 1; // 0x1
+    field public static final int UNIT_MILES = 4; // 0x4
+    field public static final int UNIT_MILES_P1 = 5; // 0x5
+    field public static final int UNIT_YARDS = 7; // 0x7
+  }
+
+  public class DistanceSpan extends android.text.style.CharacterStyle {
+    method public static androidx.car.app.model.DistanceSpan create(androidx.car.app.model.Distance);
+    method public androidx.car.app.model.Distance getDistance();
+    method public void updateDrawState(android.text.TextPaint?);
+  }
+
+  public class DurationSpan extends android.text.style.CharacterStyle {
+    method public static androidx.car.app.model.DurationSpan create(long);
+    method @RequiresApi(26) public static androidx.car.app.model.DurationSpan create(java.time.Duration);
+    method public long getDurationSeconds();
+    method public void updateDrawState(android.text.TextPaint?);
+  }
+
+  public class ForegroundCarColorSpan extends android.text.style.CharacterStyle {
+    method public static androidx.car.app.model.ForegroundCarColorSpan create(androidx.car.app.model.CarColor);
+    method public androidx.car.app.model.CarColor getColor();
+    method public void updateDrawState(android.text.TextPaint);
+  }
+
+  public class GridItem implements androidx.car.app.model.Item {
+    method public static androidx.car.app.model.GridItem.Builder builder();
+    method public androidx.car.app.model.CarIcon getImage();
+    method public int getImageType();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.CarText? getText();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public androidx.car.app.model.Toggle? getToggle();
+    field public static final int IMAGE_TYPE_ICON = 1; // 0x1
+    field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
+  }
+
+  public static final class GridItem.Builder {
+    method public androidx.car.app.model.GridItem build();
+    method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon);
+    method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
+    method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+    method public androidx.car.app.model.GridItem.Builder setText(CharSequence?);
+    method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
+    method public androidx.car.app.model.GridItem.Builder setToggle(androidx.car.app.model.Toggle?);
+  }
+
+  public final class GridTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.GridTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.CarIcon? getBackgroundImage();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.ItemList? getSingleList();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isLoading();
+  }
+
+  public static final class GridTemplate.Builder {
+    method public androidx.car.app.model.GridTemplate build();
+    method public androidx.car.app.model.GridTemplate.Builder clearAllLists();
+    method public androidx.car.app.model.GridTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.GridTemplate.Builder setBackgroundImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.model.GridTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.GridTemplate.Builder setLoading(boolean);
+    method public androidx.car.app.model.GridTemplate.Builder setSingleList(androidx.car.app.model.ItemList);
+    method public androidx.car.app.model.GridTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public interface Item {
+  }
+
+  public final class ItemList {
+    method public static androidx.car.app.model.ItemList.Builder builder();
+    method public java.util.List<java.lang.Object!> getItems();
+    method public androidx.car.app.model.CarText? getNoItemsMessage();
+    method public int getSelectedIndex();
+    method public boolean isRefresh(androidx.car.app.model.ItemList?, androidx.car.app.utils.Logger);
+  }
+
+  public static final class ItemList.Builder {
+    ctor public ItemList.Builder();
+    method public androidx.car.app.model.ItemList.Builder addItem(androidx.car.app.model.Item);
+    method public androidx.car.app.model.ItemList build();
+    method public androidx.car.app.model.ItemList.Builder clearItems();
+    method public androidx.car.app.model.ItemList.Builder setNoItemsMessage(CharSequence?);
+    method public androidx.car.app.model.ItemList.Builder setOnItemsVisibilityChangeListener(androidx.car.app.model.ItemList.OnItemVisibilityChangedListener?);
+    method public androidx.car.app.model.ItemList.Builder setSelectable(androidx.car.app.model.ItemList.OnSelectedListener?);
+    method public androidx.car.app.model.ItemList.Builder setSelectedIndex(int);
+  }
+
+  public static interface ItemList.OnItemVisibilityChangedListener {
+    method public void onItemVisibilityChanged(int, int);
+  }
+
+  public static interface ItemList.OnSelectedListener {
+    method public void onSelected(int);
+  }
+
+  public final class LatLng {
+    method public static androidx.car.app.model.LatLng create(double, double);
+    method public static androidx.car.app.model.LatLng create(android.location.Location);
+    method public double getLatitude();
+    method public double getLongitude();
+  }
+
+  public final class ListTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.ListTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public java.util.List<androidx.car.app.model.SectionedItemList!> getSectionLists();
+    method public androidx.car.app.model.ItemList? getSingleList();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isLoading();
+  }
+
+  public static final class ListTemplate.Builder {
+    method public androidx.car.app.model.ListTemplate.Builder addList(androidx.car.app.model.ItemList, CharSequence);
+    method public androidx.car.app.model.ListTemplate build();
+    method public androidx.car.app.model.ListTemplate.Builder clearAllLists();
+    method public androidx.car.app.model.ListTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.ListTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.ListTemplate.Builder setLoading(boolean);
+    method public androidx.car.app.model.ListTemplate.Builder setSingleList(androidx.car.app.model.ItemList);
+    method public androidx.car.app.model.ListTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public final class MessageTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.MessageTemplate.Builder builder(CharSequence);
+    method public androidx.car.app.model.ActionList? getActionList();
+    method public androidx.car.app.model.CarText? getDebugMessage();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public androidx.car.app.model.CarText getMessage();
+    method public androidx.car.app.model.CarText? getTitle();
+  }
+
+  public static final class MessageTemplate.Builder {
+    method public androidx.car.app.model.MessageTemplate build();
+    method public androidx.car.app.model.MessageTemplate.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+    method public androidx.car.app.model.MessageTemplate.Builder setDebugCause(Throwable?);
+    method public androidx.car.app.model.MessageTemplate.Builder setDebugMessage(String?);
+    method public androidx.car.app.model.MessageTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.MessageTemplate.Builder setIcon(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.model.MessageTemplate.Builder setMessage(CharSequence);
+    method public androidx.car.app.model.MessageTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public class Metadata {
+    method public static androidx.car.app.model.Metadata.Builder builder();
+    method public androidx.car.app.model.Place? getPlace();
+    method public androidx.car.app.model.Metadata.Builder newBuilder();
+    method public static androidx.car.app.model.Metadata ofPlace(androidx.car.app.model.Place);
+    field public static final androidx.car.app.model.Metadata EMPTY_METADATA;
+  }
+
+  public static final class Metadata.Builder {
+    method public androidx.car.app.model.Metadata build();
+    method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
+  }
+
+  public interface OnClickListener {
+    method public void onClick();
+  }
+
+  public class OnClickListenerWrapper {
+    method public boolean isParkedOnly();
+  }
+
+  public final class Pane {
+    method public static androidx.car.app.model.Pane.Builder builder();
+    method public androidx.car.app.model.ActionList? getActionList();
+    method public java.util.List<java.lang.Object!> getRows();
+    method public boolean isLoading();
+    method public boolean isRefresh(androidx.car.app.model.Pane?, androidx.car.app.utils.Logger);
+  }
+
+  public static final class Pane.Builder {
+    ctor public Pane.Builder();
+    method public androidx.car.app.model.Pane.Builder addRow(androidx.car.app.model.Row);
+    method public androidx.car.app.model.Pane build();
+    method public androidx.car.app.model.Pane.Builder clearRows();
+    method public androidx.car.app.model.Pane.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
+    method public androidx.car.app.model.Pane.Builder setLoading(boolean);
+  }
+
+  public final class PaneTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.PaneTemplate.Builder builder(androidx.car.app.model.Pane);
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.Pane getPane();
+    method public androidx.car.app.model.CarText? getTitle();
+  }
+
+  public static final class PaneTemplate.Builder {
+    method public androidx.car.app.model.PaneTemplate build();
+    method public androidx.car.app.model.PaneTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.PaneTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.PaneTemplate.Builder setPane(androidx.car.app.model.Pane);
+    method public androidx.car.app.model.PaneTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public final class ParkedOnlyOnClickListener implements androidx.car.app.model.OnClickListener {
+    method public static androidx.car.app.model.ParkedOnlyOnClickListener create(androidx.car.app.model.OnClickListener);
+    method public void onClick();
+  }
+
+  public class Place {
+    method public static androidx.car.app.model.Place.Builder builder(androidx.car.app.model.LatLng);
+    method public androidx.car.app.model.LatLng getLatLng();
+    method public androidx.car.app.model.PlaceMarker? getMarker();
+    method public androidx.car.app.model.Place.Builder newBuilder();
+  }
+
+  public static final class Place.Builder {
+    method public androidx.car.app.model.Place build();
+    method public androidx.car.app.model.Place.Builder setLatLng(androidx.car.app.model.LatLng);
+    method public androidx.car.app.model.Place.Builder setMarker(androidx.car.app.model.PlaceMarker?);
+  }
+
+  public final class PlaceListMapTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.PlaceListMapTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Place? getAnchor();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.ItemList? getItemList();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isCurrentLocationEnabled();
+    method public boolean isLoading();
+  }
+
+  public static final class PlaceListMapTemplate.Builder {
+    ctor public PlaceListMapTemplate.Builder();
+    method public androidx.car.app.model.PlaceListMapTemplate build();
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setAnchor(androidx.car.app.model.Place?);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setCurrentLocationEnabled(boolean);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setLoading(boolean);
+    method public androidx.car.app.model.PlaceListMapTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public class PlaceMarker {
+    method public static androidx.car.app.model.PlaceMarker.Builder builder();
+    method public androidx.car.app.model.CarColor? getColor();
+    method public static androidx.car.app.model.PlaceMarker getDefault();
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public int getIconType();
+    method public androidx.car.app.model.CarText? getLabel();
+    method public static boolean isDefaultMarker(androidx.car.app.model.PlaceMarker?);
+    field public static final int TYPE_ICON = 0; // 0x0
+    field public static final int TYPE_IMAGE = 1; // 0x1
+  }
+
+  public static final class PlaceMarker.Builder {
+    method public androidx.car.app.model.PlaceMarker build();
+    method public androidx.car.app.model.PlaceMarker.Builder setColor(androidx.car.app.model.CarColor?);
+    method public androidx.car.app.model.PlaceMarker.Builder setIcon(androidx.car.app.model.CarIcon?, int);
+    method public androidx.car.app.model.PlaceMarker.Builder setLabel(CharSequence?);
+  }
+
+  public class Row implements androidx.car.app.model.Item {
+    method public static androidx.car.app.model.Row.Builder builder();
+    method public int getFlags();
+    method public androidx.car.app.model.CarIcon? getImage();
+    method public androidx.car.app.model.Metadata getMetadata();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
+    method public int getRowImageType();
+    method public java.util.List<androidx.car.app.model.CarText!> getTexts();
+    method public androidx.car.app.model.CarText getTitle();
+    method public androidx.car.app.model.Toggle? getToggle();
+    method public boolean isBrowsable();
+    method public androidx.car.app.model.Row row();
+    method public void yourBoat();
+    field public static final int IMAGE_TYPE_ICON = 4; // 0x4
+    field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
+    field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
+    field public static final int ROW_FLAG_NONE = 1; // 0x1
+    field public static final int ROW_FLAG_SECTION_HEADER = 4; // 0x4
+    field public static final int ROW_FLAG_SHOW_DIVIDERS = 2; // 0x2
+  }
+
+  public static final class Row.Builder {
+    method public androidx.car.app.model.Row.Builder addText(CharSequence);
+    method public androidx.car.app.model.Row build();
+    method public androidx.car.app.model.Row.Builder clearText();
+    method public androidx.car.app.model.Row.Builder setBrowsable(boolean);
+    method public androidx.car.app.model.Row.Builder setFlags(int);
+    method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?, int);
+    method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
+    method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
+    method public androidx.car.app.model.Row.Builder setTitle(CharSequence);
+    method public androidx.car.app.model.Row.Builder setToggle(androidx.car.app.model.Toggle?);
+  }
+
+  public final class SearchTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.SearchListener);
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public String? getInitialSearchText();
+    method public androidx.car.app.model.ItemList? getItemList();
+    method public String? getSearchHint();
+    method public boolean isLoading();
+    method public boolean isShowKeyboardByDefault();
+  }
+
+  public static final class SearchTemplate.Builder {
+    method public androidx.car.app.model.SearchTemplate build();
+    method public androidx.car.app.model.SearchTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.model.SearchTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.model.SearchTemplate.Builder setInitialSearchText(String?);
+    method public androidx.car.app.model.SearchTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.model.SearchTemplate.Builder setLoading(boolean);
+    method public androidx.car.app.model.SearchTemplate.Builder setSearchHint(String?);
+    method public androidx.car.app.model.SearchTemplate.Builder setShowKeyboardByDefault(boolean);
+  }
+
+  public class SectionedItemList {
+    method public static androidx.car.app.model.SectionedItemList create(androidx.car.app.model.ItemList, androidx.car.app.model.CarText);
+    method public androidx.car.app.model.CarText getHeader();
+    method public androidx.car.app.model.ItemList getItemList();
+  }
+
+  public interface Template {
+    method public default void checkPermissions(android.content.Context);
+    method public default boolean isRefresh(androidx.car.app.model.Template, androidx.car.app.utils.Logger);
+  }
+
+  public final class TemplateInfo {
+    ctor public TemplateInfo(androidx.car.app.model.Template, String);
+    method public Class<? extends androidx.car.app.model.Template> getTemplateClass();
+    method public String getTemplateId();
+  }
+
+  public final class TemplateWrapper {
+    method public static androidx.car.app.model.TemplateWrapper copyOf(androidx.car.app.model.TemplateWrapper);
+    method public int getCurrentTaskStep();
+    method public String getId();
+    method public androidx.car.app.model.Template getTemplate();
+    method public java.util.List<androidx.car.app.model.TemplateInfo!>? getTemplateInfosForScreenStack();
+    method public boolean isRefresh();
+    method public void setCurrentTaskStep(int);
+    method public void setId(String);
+    method public void setRefresh(boolean);
+    method public void setTemplate(androidx.car.app.model.Template);
+    method public static androidx.car.app.model.TemplateWrapper wrap(androidx.car.app.model.Template);
+    method public static androidx.car.app.model.TemplateWrapper wrap(androidx.car.app.model.Template, String);
+  }
+
+  public class Toggle {
+    method public static androidx.car.app.model.Toggle.Builder builder(androidx.car.app.model.Toggle.OnCheckedChangeListener);
+    method public boolean isChecked();
+  }
+
+  public static final class Toggle.Builder {
+    method public androidx.car.app.model.Toggle build();
+    method public androidx.car.app.model.Toggle.Builder setChecked(boolean);
+    method public androidx.car.app.model.Toggle.Builder setCheckedChangeListener(androidx.car.app.model.Toggle.OnCheckedChangeListener);
+  }
+
+  public static interface Toggle.OnCheckedChangeListener {
+    method public void onCheckedChange(boolean);
+  }
+
+}
+
+package androidx.car.app.model.constraints {
+
+  public class ActionsConstraints {
+    method @VisibleForTesting public static androidx.car.app.model.constraints.ActionsConstraints.Builder builder();
+    method public java.util.Set<java.lang.Integer!> getDisallowedActionTypes();
+    method public int getMaxActions();
+    method public int getMaxCustomTitles();
+    method public java.util.Set<java.lang.Integer!> getRequiredActionTypes();
+    method @VisibleForTesting public androidx.car.app.model.constraints.ActionsConstraints.Builder newBuilder();
+    method public void validateOrThrow(java.util.List<java.lang.Object!>);
+    field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_HEADER;
+    field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION;
+    field public static final androidx.car.app.model.constraints.ActionsConstraints ACTIONS_CONSTRAINTS_SIMPLE;
+  }
+
+  @VisibleForTesting public static final class ActionsConstraints.Builder {
+    method public androidx.car.app.model.constraints.ActionsConstraints.Builder addDisallowedActionType(int);
+    method public androidx.car.app.model.constraints.ActionsConstraints.Builder addRequiredActionType(int);
+    method public androidx.car.app.model.constraints.ActionsConstraints build();
+    method public androidx.car.app.model.constraints.ActionsConstraints.Builder setMaxActions(int);
+    method public androidx.car.app.model.constraints.ActionsConstraints.Builder setMaxCustomTitles(int);
+  }
+
+  public class CarColorConstraints {
+    method public void validateOrThrow(androidx.car.app.model.CarColor);
+    field public static final androidx.car.app.model.constraints.CarColorConstraints STANDARD_ONLY;
+    field public static final androidx.car.app.model.constraints.CarColorConstraints UNCONSTRAINED;
+  }
+
+  public class CarIconConstraints {
+    method public androidx.core.graphics.drawable.IconCompat checkSupportedIcon(androidx.core.graphics.drawable.IconCompat);
+    method public void validateOrThrow(androidx.car.app.model.CarIcon?);
+    field public static final androidx.car.app.model.constraints.CarIconConstraints DEFAULT;
+    field public static final androidx.car.app.model.constraints.CarIconConstraints UNCONSTRAINED;
+  }
+
+  public class RowConstraints {
+    method public static androidx.car.app.model.constraints.RowConstraints.Builder builder();
+    method public androidx.car.app.model.constraints.CarIconConstraints getCarIconConstraints();
+    method public int getFlagOverrides();
+    method public int getMaxActionsExclusive();
+    method public int getMaxTextLinesPerRow();
+    method public boolean isImageAllowed();
+    method public boolean isOnClickListenerAllowed();
+    method public boolean isToggleAllowed();
+    method public androidx.car.app.model.constraints.RowConstraints.Builder newBuilder();
+    method public void validateOrThrow(Object);
+    field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_CONSERVATIVE;
+    field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_FULL_LIST;
+    field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_PANE;
+    field public static final androidx.car.app.model.constraints.RowConstraints ROW_CONSTRAINTS_SIMPLE;
+    field public static final androidx.car.app.model.constraints.RowConstraints UNCONSTRAINED;
+  }
+
+  public static final class RowConstraints.Builder {
+    method public androidx.car.app.model.constraints.RowConstraints build();
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setCarIconConstraints(androidx.car.app.model.constraints.CarIconConstraints);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setFlagOverrides(int);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setImageAllowed(boolean);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxActionsExclusive(int);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxTextLinesPerRow(int);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setOnClickListenerAllowed(boolean);
+    method public androidx.car.app.model.constraints.RowConstraints.Builder setToggleAllowed(boolean);
+  }
+
+  public class RowListConstraints {
+    method public static androidx.car.app.model.constraints.RowListConstraints.Builder builder();
+    method public int getMaxActions();
+    method public androidx.car.app.model.constraints.RowConstraints getRowConstraints();
+    method public int getRowListType();
+    method public boolean isAllowSelectableLists();
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder newBuilder();
+    method public void validateOrThrow(androidx.car.app.model.ItemList);
+    method public void validateOrThrow(java.util.List<androidx.car.app.model.SectionedItemList!>);
+    method public void validateOrThrow(androidx.car.app.model.Pane);
+    field public static final int DEFAULT_LIST = 0; // 0x0
+    field public static final int PANE = 1; // 0x1
+    field public static final int ROUTE_PREVIEW = 2; // 0x2
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_CONSERVATIVE;
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_FULL_LIST;
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_PANE;
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW;
+    field public static final androidx.car.app.model.constraints.RowListConstraints ROW_LIST_CONSTRAINTS_SIMPLE;
+  }
+
+  public static final class RowListConstraints.Builder {
+    method public androidx.car.app.model.constraints.RowListConstraints build();
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder setAllowSelectableLists(boolean);
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder setMaxActions(int);
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder setRowConstraints(androidx.car.app.model.constraints.RowConstraints);
+    method public androidx.car.app.model.constraints.RowListConstraints.Builder setRowListType(int);
+  }
+
+}
+
+package androidx.car.app.navigation {
+
+  public class NavigationManager {
+    method @MainThread public void navigationEnded();
+    method @MainThread public void navigationStarted();
+    method @MainThread public void setListener(androidx.car.app.navigation.NavigationManagerListener?);
+    method @MainThread public void updateTrip(androidx.car.app.navigation.model.Trip);
+  }
+
+  public interface NavigationManagerListener {
+    method public void onAutoDriveEnabled();
+    method public void stopNavigation();
+  }
+
+}
+
+package androidx.car.app.navigation.model {
+
+  public final class Destination {
+    method public static androidx.car.app.navigation.model.Destination.Builder builder(CharSequence, CharSequence);
+    method public static androidx.car.app.navigation.model.Destination.Builder builder();
+    method public androidx.car.app.model.CarText? getAddress();
+    method public androidx.car.app.model.CarIcon? getImage();
+    method public androidx.car.app.model.CarText? getName();
+  }
+
+  public static final class Destination.Builder {
+    method public androidx.car.app.navigation.model.Destination build();
+    method public androidx.car.app.navigation.model.Destination.Builder setAddress(CharSequence?);
+    method public androidx.car.app.navigation.model.Destination.Builder setImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.Destination.Builder setName(CharSequence?);
+  }
+
+  public final class Lane {
+    method public static androidx.car.app.navigation.model.Lane.Builder builder();
+    method public java.util.List<androidx.car.app.navigation.model.LaneDirection!> getDirections();
+  }
+
+  public static final class Lane.Builder {
+    ctor public Lane.Builder();
+    method public androidx.car.app.navigation.model.Lane.Builder addDirection(androidx.car.app.navigation.model.LaneDirection);
+    method public androidx.car.app.navigation.model.Lane build();
+    method public androidx.car.app.navigation.model.Lane.Builder clearDirections();
+  }
+
+  public final class LaneDirection {
+    method public static androidx.car.app.navigation.model.LaneDirection create(int, boolean);
+    method public int getShape();
+    method public boolean isHighlighted();
+    field public static final int SHAPE_NORMAL_LEFT = 5; // 0x5
+    field public static final int SHAPE_NORMAL_RIGHT = 6; // 0x6
+    field public static final int SHAPE_SHARP_LEFT = 7; // 0x7
+    field public static final int SHAPE_SHARP_RIGHT = 8; // 0x8
+    field public static final int SHAPE_SLIGHT_LEFT = 3; // 0x3
+    field public static final int SHAPE_SLIGHT_RIGHT = 4; // 0x4
+    field public static final int SHAPE_STRAIGHT = 2; // 0x2
+    field public static final int SHAPE_UNKNOWN = 1; // 0x1
+    field public static final int SHAPE_U_TURN_LEFT = 9; // 0x9
+    field public static final int SHAPE_U_TURN_RIGHT = 10; // 0xa
+  }
+
+  public final class Maneuver {
+    method public static androidx.car.app.navigation.model.Maneuver.Builder builder(int);
+    method public androidx.car.app.model.CarIcon? getIcon();
+    method public int getRoundaboutExitAngle();
+    method public int getRoundaboutExitNumber();
+    method public int getType();
+    field public static final int TYPE_DEPART = 1; // 0x1
+    field public static final int TYPE_DESTINATION = 39; // 0x27
+    field public static final int TYPE_DESTINATION_LEFT = 41; // 0x29
+    field public static final int TYPE_DESTINATION_RIGHT = 42; // 0x2a
+    field public static final int TYPE_DESTINATION_STRAIGHT = 40; // 0x28
+    field public static final int TYPE_FERRY_BOAT = 37; // 0x25
+    field public static final int TYPE_FERRY_TRAIN = 38; // 0x26
+    field public static final int TYPE_FORK_LEFT = 25; // 0x19
+    field public static final int TYPE_FORK_RIGHT = 26; // 0x1a
+    field public static final int TYPE_KEEP_LEFT = 3; // 0x3
+    field public static final int TYPE_KEEP_RIGHT = 4; // 0x4
+    field public static final int TYPE_MERGE_LEFT = 27; // 0x1b
+    field public static final int TYPE_MERGE_RIGHT = 28; // 0x1c
+    field public static final int TYPE_MERGE_SIDE_UNSPECIFIED = 29; // 0x1d
+    field public static final int TYPE_NAME_CHANGE = 2; // 0x2
+    field public static final int TYPE_OFF_RAMP_NORMAL_LEFT = 23; // 0x17
+    field public static final int TYPE_OFF_RAMP_NORMAL_RIGHT = 24; // 0x18
+    field public static final int TYPE_OFF_RAMP_SLIGHT_LEFT = 21; // 0x15
+    field public static final int TYPE_OFF_RAMP_SLIGHT_RIGHT = 22; // 0x16
+    field public static final int TYPE_ON_RAMP_NORMAL_LEFT = 15; // 0xf
+    field public static final int TYPE_ON_RAMP_NORMAL_RIGHT = 16; // 0x10
+    field public static final int TYPE_ON_RAMP_SHARP_LEFT = 17; // 0x11
+    field public static final int TYPE_ON_RAMP_SHARP_RIGHT = 18; // 0x12
+    field public static final int TYPE_ON_RAMP_SLIGHT_LEFT = 13; // 0xd
+    field public static final int TYPE_ON_RAMP_SLIGHT_RIGHT = 14; // 0xe
+    field public static final int TYPE_ON_RAMP_U_TURN_LEFT = 19; // 0x13
+    field public static final int TYPE_ON_RAMP_U_TURN_RIGHT = 20; // 0x14
+    field public static final int TYPE_ROUNDABOUT_ENTER = 30; // 0x1e
+    field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW = 34; // 0x22
+    field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE = 35; // 0x23
+    field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW = 32; // 0x20
+    field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE = 33; // 0x21
+    field public static final int TYPE_ROUNDABOUT_EXIT = 31; // 0x1f
+    field public static final int TYPE_STRAIGHT = 36; // 0x24
+    field public static final int TYPE_TURN_NORMAL_LEFT = 7; // 0x7
+    field public static final int TYPE_TURN_NORMAL_RIGHT = 8; // 0x8
+    field public static final int TYPE_TURN_SHARP_LEFT = 9; // 0x9
+    field public static final int TYPE_TURN_SHARP_RIGHT = 10; // 0xa
+    field public static final int TYPE_TURN_SLIGHT_LEFT = 5; // 0x5
+    field public static final int TYPE_TURN_SLIGHT_RIGHT = 6; // 0x6
+    field public static final int TYPE_UNKNOWN = 0; // 0x0
+    field public static final int TYPE_U_TURN_LEFT = 11; // 0xb
+    field public static final int TYPE_U_TURN_RIGHT = 12; // 0xc
+  }
+
+  public static final class Maneuver.Builder {
+    method public androidx.car.app.navigation.model.Maneuver build();
+    method public androidx.car.app.navigation.model.Maneuver.Builder setIcon(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.Maneuver.Builder setRoundaboutExitAngle(int);
+    method public androidx.car.app.navigation.model.Maneuver.Builder setRoundaboutExitNumber(int);
+  }
+
+  public class MessageInfo implements androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo {
+    method public static androidx.car.app.navigation.model.MessageInfo.Builder builder(CharSequence);
+    method public androidx.car.app.model.CarIcon? getImage();
+    method public androidx.car.app.model.CarText? getText();
+    method public androidx.car.app.model.CarText getTitle();
+  }
+
+  public static final class MessageInfo.Builder {
+    method public androidx.car.app.navigation.model.MessageInfo build();
+    method public androidx.car.app.navigation.model.MessageInfo.Builder setImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.MessageInfo.Builder setText(CharSequence?);
+    method public androidx.car.app.navigation.model.MessageInfo.Builder setTitle(CharSequence);
+  }
+
+  public class NavigationTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.navigation.model.NavigationTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip getActionStrip();
+    method public androidx.car.app.model.CarColor? getBackgroundColor();
+    method public androidx.car.app.navigation.model.TravelEstimate? getDestinationTravelEstimate();
+    method public androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo? getNavigationInfo();
+  }
+
+  public static final class NavigationTemplate.Builder {
+    method public androidx.car.app.navigation.model.NavigationTemplate build();
+    method public androidx.car.app.navigation.model.NavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip);
+    method public androidx.car.app.navigation.model.NavigationTemplate.Builder setBackgroundColor(androidx.car.app.model.CarColor?);
+    method public androidx.car.app.navigation.model.NavigationTemplate.Builder setDestinationTravelEstimate(androidx.car.app.navigation.model.TravelEstimate?);
+    method public androidx.car.app.navigation.model.NavigationTemplate.Builder setNavigationInfo(androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo?);
+  }
+
+  public static interface NavigationTemplate.NavigationInfo {
+  }
+
+  public final class PlaceListNavigationTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.ItemList? getItemList();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isLoading();
+  }
+
+  public static final class PlaceListNavigationTemplate.Builder {
+    ctor public PlaceListNavigationTemplate.Builder();
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate build();
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setIsLoading(boolean);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+    method @VisibleForTesting public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public final class RoutePreviewNavigationTemplate implements androidx.car.app.model.Template {
+    method public static androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder builder();
+    method public androidx.car.app.model.ActionStrip? getActionStrip();
+    method public androidx.car.app.model.Action? getHeaderAction();
+    method public androidx.car.app.model.ItemList? getItemList();
+    method public androidx.car.app.model.Action? getNavigateAction();
+    method public androidx.car.app.model.CarText? getTitle();
+    method public boolean isLoading();
+  }
+
+  public static final class RoutePreviewNavigationTemplate.Builder {
+    ctor public RoutePreviewNavigationTemplate.Builder();
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate build();
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setIsLoading(boolean);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
+    method @VisibleForTesting public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence?);
+  }
+
+  public class RoutingInfo implements androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo {
+    method public static androidx.car.app.navigation.model.RoutingInfo.Builder builder();
+    method public androidx.car.app.model.Distance? getCurrentDistance();
+    method public androidx.car.app.navigation.model.Step? getCurrentStep();
+    method public androidx.car.app.model.CarIcon? getJunctionImage();
+    method public androidx.car.app.navigation.model.Step? getNextStep();
+    method public boolean isLoading();
+  }
+
+  public static final class RoutingInfo.Builder {
+    method public androidx.car.app.navigation.model.RoutingInfo build();
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setCurrentStep(androidx.car.app.navigation.model.Step, androidx.car.app.model.Distance);
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setIsLoading(boolean);
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setJunctionImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setNextStep(androidx.car.app.navigation.model.Step?);
+  }
+
+  public final class Step {
+    method public static androidx.car.app.navigation.model.Step.Builder builder(CharSequence);
+    method public androidx.car.app.model.CarText? getCue();
+    method public java.util.List<androidx.car.app.navigation.model.Lane!> getLanes();
+    method public androidx.car.app.model.CarIcon? getLanesImage();
+    method public androidx.car.app.navigation.model.Maneuver? getManeuver();
+    method public androidx.car.app.model.CarText? getRoad();
+    method public androidx.car.app.navigation.model.Step.Builder newBuilder();
+  }
+
+  public static final class Step.Builder {
+    method public androidx.car.app.navigation.model.Step.Builder addLane(androidx.car.app.navigation.model.Lane);
+    method public androidx.car.app.navigation.model.Step build();
+    method public androidx.car.app.navigation.model.Step.Builder clearLanes();
+    method public androidx.car.app.navigation.model.Step.Builder setCue(CharSequence);
+    method public androidx.car.app.navigation.model.Step.Builder setLanesImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.Step.Builder setManeuver(androidx.car.app.navigation.model.Maneuver?);
+    method public androidx.car.app.navigation.model.Step.Builder setRoad(CharSequence);
+  }
+
+  public final class TravelEstimate {
+    method public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
+    method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+    method public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
+    method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+    method public androidx.car.app.model.DateTimeWithZone? getArrivalTimeAtDestination();
+    method public androidx.car.app.model.Distance getRemainingDistance();
+    method public androidx.car.app.model.CarColor getRemainingDistanceColor();
+    method public androidx.car.app.model.CarColor getRemainingTimeColor();
+    method public long getRemainingTimeSeconds();
+  }
+
+  public static final class TravelEstimate.Builder {
+    method public androidx.car.app.navigation.model.TravelEstimate build();
+    method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingDistanceColor(androidx.car.app.model.CarColor);
+    method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTimeColor(androidx.car.app.model.CarColor);
+  }
+
+  public final class Trip {
+    method public static androidx.car.app.navigation.model.Trip.Builder builder();
+    method public androidx.car.app.model.CarText? getCurrentRoad();
+    method public java.util.List<androidx.car.app.navigation.model.TravelEstimate!> getDestinationTravelEstimates();
+    method public java.util.List<androidx.car.app.navigation.model.Destination!> getDestinations();
+    method public java.util.List<androidx.car.app.navigation.model.TravelEstimate!> getStepTravelEstimates();
+    method public java.util.List<androidx.car.app.navigation.model.Step!> getSteps();
+    method public boolean isLoading();
+  }
+
+  public static final class Trip.Builder {
+    ctor public Trip.Builder();
+    method public androidx.car.app.navigation.model.Trip.Builder addDestination(androidx.car.app.navigation.model.Destination);
+    method public androidx.car.app.navigation.model.Trip.Builder addDestinationTravelEstimate(androidx.car.app.navigation.model.TravelEstimate);
+    method public androidx.car.app.navigation.model.Trip.Builder addStep(androidx.car.app.navigation.model.Step?);
+    method public androidx.car.app.navigation.model.Trip.Builder addStepTravelEstimate(androidx.car.app.navigation.model.TravelEstimate);
+    method public androidx.car.app.navigation.model.Trip build();
+    method public androidx.car.app.navigation.model.Trip.Builder clearDestinationTravelEstimates();
+    method public androidx.car.app.navigation.model.Trip.Builder clearDestinations();
+    method public androidx.car.app.navigation.model.Trip.Builder clearStepTravelEstimates();
+    method public androidx.car.app.navigation.model.Trip.Builder clearSteps();
+    method public androidx.car.app.navigation.model.Trip.Builder setCurrentRoad(CharSequence?);
+    method public androidx.car.app.navigation.model.Trip.Builder setIsLoading(boolean);
+  }
+
+}
+
+package androidx.car.app.notification {
+
+  public class CarAppExtender implements androidx.core.app.NotificationCompat.Extender {
+    ctor public CarAppExtender(android.app.Notification);
+    method public static androidx.car.app.notification.CarAppExtender.Builder builder();
+    method public androidx.core.app.NotificationCompat.Builder extend(androidx.core.app.NotificationCompat.Builder);
+    method public java.util.List<android.app.Notification.Action!> getActions();
+    method public android.app.PendingIntent? getContentIntent();
+    method public CharSequence? getContentText();
+    method public CharSequence? getContentTitle();
+    method public android.app.PendingIntent? getDeleteIntent();
+    method public int getImportance();
+    method public android.graphics.Bitmap? getLargeIconBitmap();
+    method public int getSmallIconResId();
+    method public boolean isExtended();
+    method public static boolean isExtended(android.app.Notification);
+  }
+
+  public static final class CarAppExtender.Builder {
+    ctor public CarAppExtender.Builder();
+    method public androidx.car.app.notification.CarAppExtender.Builder addAction(@DrawableRes int, CharSequence, android.app.PendingIntent);
+    method public androidx.car.app.notification.CarAppExtender build();
+    method public androidx.car.app.notification.CarAppExtender.Builder clearActions();
+    method public androidx.car.app.notification.CarAppExtender.Builder setContentIntent(android.app.PendingIntent?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setContentText(CharSequence?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setContentTitle(CharSequence?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setDeleteIntent(android.app.PendingIntent?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setImportance(int);
+    method public androidx.car.app.notification.CarAppExtender.Builder setLargeIcon(android.graphics.Bitmap?);
+    method public androidx.car.app.notification.CarAppExtender.Builder setSmallIcon(int);
+  }
+
+}
+
 package androidx.car.app.serialization {
 
   public final class Bundleable implements android.os.Parcelable {
@@ -171,6 +1085,10 @@
 
 package androidx.car.app.utils {
 
+  public interface Logger {
+    method public void log(String);
+  }
+
   public class ThreadUtils {
     method public static void checkMainThread();
     method public static void runOnMain(Runnable);
diff --git a/car/app/app/build.gradle b/car/app/app/build.gradle
index 1b0f2e5..16e0e08 100644
--- a/car/app/app/build.gradle
+++ b/car/app/app/build.gradle
@@ -13,10 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 import androidx.build.LibraryGroups
 import androidx.build.LibraryType
-import androidx.build.LibraryVersions
-import androidx.build.Publish
+
+import static androidx.build.dependencies.DependenciesKt.*
 
 plugins {
     id("AndroidXPlugin")
@@ -26,13 +27,38 @@
 dependencies {
     implementation "androidx.activity:activity:1.1.0"
     implementation "androidx.annotation:annotation:1.1.0"
+    implementation "androidx.core:core:1.3.0"
     implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"
     implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
+
+    androidTestImplementation(ANDROIDX_TEST_CORE)
+    androidTestImplementation(ANDROIDX_TEST_RULES)
+    androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    // TODO(shiufai): We need this for assertThrows. Point back to the AndroidX shared version if
+    // it is ever upgraded.
+    androidTestImplementation("junit:junit:4.13")
+    androidTestImplementation(TRUTH)
+    androidTestImplementation(MOCKITO_ANDROID)
+    androidTestImplementation ROBOLECTRIC, {
+        // The following are excluded as they are resulting in duplicated class conflicts with
+        // the junit dependency above.
+        exclude group: "org.apache.maven.wagon"
+        exclude group: "org.apache.maven"
+    }
 }
 
 android {
     defaultConfig {
         minSdkVersion 21
+        multiDexEnabled = true
+    }
+    lintOptions {
+        // We have a bunch of builder/inner classes where the outer classes access the private
+        // fields/constructors directly.
+        disable("SyntheticAccessor")
+        // We rely on keeping a bunch of private variables in the library for serialization.
+        disable("BanKeepAnnotation")
     }
     buildFeatures {
         aidl = true
@@ -40,6 +66,8 @@
     buildTypes.all {
         consumerProguardFiles 'proguard-rules.pro'
     }
+
+    testOptions.unitTests.includeAndroidResources = true
 }
 
 androidx {
diff --git a/car/app/app/src/androidTest/AndroidManifest.xml b/car/app/app/src/androidTest/AndroidManifest.xml
index ce015a5..d84e190 100644
--- a/car/app/app/src/androidTest/AndroidManifest.xml
+++ b/car/app/app/src/androidTest/AndroidManifest.xml
@@ -15,6 +15,6 @@
   limitations under the License.
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.car.app.test">
+    package="androidx.car.app">
 
 </manifest>
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/TestData.java b/car/app/app/src/androidTest/java/androidx/car/app/TestData.java
new file mode 100644
index 0000000..2017796
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/TestData.java
@@ -0,0 +1,32 @@
+/*
+ * 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.car.app;
+
+import androidx.car.app.model.LatLng;
+import androidx.car.app.model.Place;
+
+/** A grab bag of fake data shared by tests. */
+public final class TestData {
+    public static final LatLng GOOGLE_KIR = LatLng.create(47.6696482, -122.19950278);
+    public static final LatLng GOOGLE_BVE = LatLng.create(47.6204588, -122.1918818);
+
+    public static final Place PLACE_KIR = Place.builder(GOOGLE_KIR).build();
+    public static final Place PLACE_BVE = Place.builder(GOOGLE_BVE).build();
+
+    private TestData() {
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/TestUtils.java b/car/app/app/src/androidTest/java/androidx/car/app/TestUtils.java
new file mode 100644
index 0000000..78d3eb4
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/TestUtils.java
@@ -0,0 +1,166 @@
+/*
+ * 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.car.app;
+
+import static androidx.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.text.SpannableString;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.DateTimeWithZone;
+import androidx.car.app.model.DistanceSpan;
+import androidx.car.app.model.GridItem;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.Pane;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.SectionedItemList;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.ZonedDateTime;
+import java.time.format.TextStyle;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/** A grab bag of utility methods intended only for tests. */
+public class TestUtils {
+    /** Helper functions in here only. */
+    private TestUtils() {
+    }
+
+    /**
+     * Returns a {@link DateTimeWithZone} instance from a date string and a time zone id.
+     *
+     * @param dateTimeString The string in ISO format, for example "2020-04-14T15:57:00".
+     * @param zoneIdString   An Olson DB time zone identifier, for example "US/Pacific".
+     */
+    public static DateTimeWithZone createDateTimeWithZone(
+            String dateTimeString, String zoneIdString) {
+        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+        TimeZone timeZone = TimeZone.getTimeZone(zoneIdString);
+        dateFormat.setTimeZone(timeZone);
+        Date date;
+        try {
+            date = dateFormat.parse(dateTimeString);
+        } catch (ParseException e) {
+            throw new IllegalArgumentException("Failed to parse string: " + dateTimeString, e);
+        }
+        if (date == null) {
+            throw new IllegalArgumentException("Failed to parse string: " + dateTimeString);
+        }
+        return DateTimeWithZone.create(date.getTime(), timeZone);
+    }
+
+    /** Returns a default {@link Action} instance. */
+    public static Action createAction(@Nullable String title, @Nullable CarIcon icon) {
+        return Action.builder().setTitle(title).setIcon(icon).setOnClickListener(() -> {
+        }).build();
+    }
+
+    /** Returns an {@link ItemList} with the given number of rows and selectable state. */
+    public static ItemList createItemList(int rowCount, boolean isSelectable) {
+        return createItemListWithDistanceSpan(rowCount, isSelectable, null);
+    }
+
+    /**
+     * Returns an {@link ItemList} with the given selectable state and number of rows populated with
+     * the given {@link DistanceSpan}.
+     */
+    public static ItemList createItemListWithDistanceSpan(
+            int rowCount, boolean isSelectable, @Nullable DistanceSpan distanceSpan) {
+        ItemList.Builder builder = ItemList.builder();
+        for (int i = 0; i < rowCount; ++i) {
+            Row.Builder rowBuilder = Row.builder();
+            if (distanceSpan != null) {
+                SpannableString title = new SpannableString("  title " + i);
+                title.setSpan(distanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+                rowBuilder.setTitle(title);
+            } else {
+                rowBuilder.setTitle("title " + i);
+            }
+            builder.addItem(rowBuilder.build());
+        }
+
+        if (isSelectable) {
+            builder.setSelectable(index -> {
+            });
+        }
+
+        return builder.build();
+    }
+
+    /** Returns a {@link Pane} with the given number of rows and actions */
+    public static Pane createPane(int rowCount, int actionCount) {
+        Pane.Builder builder = Pane.builder();
+        for (int i = 0; i < rowCount; ++i) {
+            builder.addRow(Row.builder().setTitle("title " + i).build());
+        }
+
+        List<Action> actions = new ArrayList<>();
+        for (int i = 0; i < actionCount; i++) {
+            actions.add(createAction("action " + i, null));
+        }
+        builder.setActions(actions);
+
+        return builder.build();
+    }
+
+    /** Returns a list of {@link SectionedItemList} with the given parameters. */
+    public static List<SectionedItemList> createSections(
+            int sectionCount, int rowCountPerSection, boolean isSelectable) {
+        List<SectionedItemList> sections = new ArrayList<>();
+
+        for (int i = 0; i < sectionCount; i++) {
+            sections.add(
+                    SectionedItemList.create(
+                            createItemList(rowCountPerSection, isSelectable),
+                            CarText.create("Section " + i)));
+        }
+
+        return sections;
+    }
+
+    /** Returns an {@link ItemList} consisting of {@link GridItem}s */
+    public static ItemList getGridItemList(int itemCount) {
+        ItemList.Builder builder = ItemList.builder();
+        while (itemCount-- > 0) {
+            builder.addItem(GridItem.builder().setTitle("Title").setImage(BACK).build());
+        }
+        return builder.build();
+    }
+
+    @RequiresApi(26)
+    public static void assertDateTimeWithZoneEquals(
+            ZonedDateTime zonedDateTime, DateTimeWithZone dateTimeWithZone) {
+        assertThat(dateTimeWithZone.getZoneShortName())
+                .isEqualTo(zonedDateTime.getZone().getDisplayName(TextStyle.SHORT,
+                        Locale.getDefault()));
+        assertThat(dateTimeWithZone.getZoneOffsetSeconds())
+                .isEqualTo(dateTimeWithZone.getZoneOffsetSeconds());
+        assertThat(dateTimeWithZone.getTimeSinceEpochMillis())
+                .isEqualTo(dateTimeWithZone.getTimeSinceEpochMillis());
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
new file mode 100644
index 0000000..e6efb98
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link ActionStrip}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActionStripTest {
+    @Test
+    public void createEmpty_throws() {
+        assertThrows(IllegalStateException.class, () -> ActionStrip.builder().build());
+    }
+
+    @Test
+    public void addDuplicatedTypes_throws() {
+        Action action1 = Action.BACK;
+        Action action2 = Action.builder().setTitle("Test").setOnClickListener(() -> {
+        }).build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ActionStrip.builder().addAction(action1).addAction(action1));
+
+        // Duplicated custom types will not throw.
+        ActionStrip.builder().addAction(action1).addAction(action2).addAction(action2).build();
+    }
+
+    @Test
+    public void createActions() {
+        Action action1 = Action.BACK;
+        Action action2 = Action.builder().setTitle("Test").setOnClickListener(() -> {
+        }).build();
+        ActionStrip list = ActionStrip.builder().addAction(action1).addAction(action2).build();
+
+        assertThat(list.getActions()).hasSize(2);
+        assertThat(action1).isEqualTo(list.getActions().get(0));
+        assertThat(action2).isEqualTo(list.getActions().get(1));
+    }
+
+    @Test
+    public void clearActions() {
+        Action action1 = Action.BACK;
+        Action action2 = Action.APP_ICON;
+        ActionStrip list =
+                ActionStrip.builder()
+                        .addAction(action1)
+                        .addAction(action2)
+                        .clearActions()
+                        .addAction(action2)
+                        .build();
+        assertThat(list.getActions()).hasSize(1);
+    }
+
+    @Test
+    public void getActionOfType() {
+        Action action1 = Action.BACK;
+        Action action2 = Action.builder().setTitle("Test").setOnClickListener(() -> {
+        }).build();
+        ActionStrip list = ActionStrip.builder().addAction(action1).addAction(action2).build();
+
+        assertThat(list.getActionOfType(Action.TYPE_BACK)).isEqualTo(action1);
+        assertThat(list.getActionOfType(Action.TYPE_CUSTOM)).isEqualTo(action2);
+    }
+
+    @Test
+    public void equals() {
+        Action action1 = Action.BACK;
+        Action action2 = Action.APP_ICON;
+        ActionStrip list = ActionStrip.builder().addAction(action1).addAction(action2).build();
+        ActionStrip list2 = ActionStrip.builder().addAction(action1).addAction(action2).build();
+
+        assertThat(list2).isEqualTo(list);
+    }
+
+    @Test
+    public void notEquals() {
+        Action action1 = Action.BACK;
+        Action action2 = Action.APP_ICON;
+        ActionStrip list = ActionStrip.builder().addAction(action1).addAction(action2).build();
+        ActionStrip list2 = ActionStrip.builder().addAction(action2).build();
+
+        assertThat(list).isNotEqualTo(list2);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ActionTest.java
new file mode 100644
index 0000000..7ac962a
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ActionTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/** Tests for {@link Action}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActionTest {
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private IOnDoneCallback.Stub mMockOnDoneCallback;
+
+    @Test
+    public void create_throws_noTitleOrIcon() {
+        assertThrows(
+                IllegalStateException.class, () -> Action.builder().setOnClickListener(() -> {
+                }).build());
+        assertThrows(
+                IllegalStateException.class,
+                () -> Action.builder().setOnClickListener(() -> {
+                }).setTitle("").build());
+    }
+
+    @Test
+    public void create_throws_invalid_carIcon() {
+        Uri.Builder builder = new Uri.Builder();
+        builder.scheme(ContentResolver.SCHEME_CONTENT);
+        builder.appendPath("foo/bar");
+        Uri iconUri = builder.build();
+        CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+
+        assertThrows(IllegalArgumentException.class, () -> Action.builder().setIcon(carIcon));
+    }
+
+    @Test
+    public void create_throws_customBackgroundColor() {
+        OnClickListener >
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        Action.builder()
+                                .setTitle("foo")
+                                .setOnClickListener(onClickListener)
+                                .setBackgroundColor(CarColor.createCustom(0xdead, 0xbeef))
+                                .build());
+    }
+
+    @Test
+    public void create_noTitleDefault() {
+        OnClickListener >
+        Action action =
+                Action.builder()
+                        .setIcon(
+                                CarIcon.of(
+                                        IconCompat.createWithResource(
+                                                ApplicationProvider.getApplicationContext(),
+                                                R.drawable.ic_test_1)))
+                        .setOnClickListener(onClickListener)
+                        .build();
+        assertThat(action.getTitle()).isNull();
+    }
+
+    @Test
+    public void create_noIconDefault() {
+        OnClickListener >
+        Action action = Action.builder().setTitle("foo").setOnClickListener(
+                onClickListener).build();
+        assertThat(action.getIcon()).isNull();
+    }
+
+    @Test
+    public void create_noBackgroundColorDefault() {
+        OnClickListener >
+        Action action = Action.builder().setTitle("foo").setOnClickListener(
+                onClickListener).build();
+        assertThat(action.getBackgroundColor()).isEqualTo(CarColor.DEFAULT);
+    }
+
+    @Test
+    public void createInstance() throws RemoteException {
+        OnClickListener >
+        IconCompat icon =
+                IconCompat.createWithResource(
+                        ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+        String title = "foo";
+        Action action =
+                Action.builder()
+                        .setTitle(title)
+                        .setIcon(CarIcon.of(icon))
+                        .setBackgroundColor(CarColor.BLUE)
+                        .setOnClickListener(onClickListener)
+                        .build();
+        assertThat(icon).isEqualTo(action.getIcon().getIcon());
+        assertThat(CarText.create(title)).isEqualTo(action.getTitle());
+        assertThat(CarColor.BLUE).isEqualTo(action.getBackgroundColor());
+        // TODO(shiufai): revisit the following as the test is not running on the main looper
+        //  thread, and thus the verify is failing.
+//        action.getOnClickListener().getListener().onClick(mockOnDoneCallback);
+//        verify(onClickListener).onClick();
+    }
+
+    @Test
+    public void create_invalidSetOnBackThrows() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> Action.BACK.newBuilder().setOnClickListener(() -> {
+                }).build());
+        assertThrows(
+                IllegalStateException.class,
+                () -> Action.BACK.newBuilder().setTitle("BACK").build());
+        assertThrows(
+                IllegalStateException.class,
+                () -> Action.BACK.newBuilder().setIcon(CarIcon.ALERT).build());
+    }
+
+    @Test
+    public void create_invalidSetOnAppIconThrows() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> Action.APP_ICON.newBuilder().setOnClickListener(() -> {
+                }).build());
+        assertThrows(
+                IllegalStateException.class,
+                () -> Action.APP_ICON.newBuilder().setTitle("APP").build());
+        assertThrows(
+                IllegalStateException.class,
+                () -> Action.APP_ICON.newBuilder().setIcon(CarIcon.ALERT).build());
+    }
+
+    @Test
+    public void equals() {
+        String title = "foo";
+        CarIcon icon = CarIcon.ALERT;
+
+        Action action1 =
+                Action.builder().setOnClickListener(() -> {
+                }).setTitle(title).setIcon(icon).build();
+        Action action2 =
+                Action.builder().setOnClickListener(() -> {
+                }).setTitle(title).setIcon(icon).build();
+
+        assertThat(action2).isEqualTo(action1);
+    }
+
+    @Test
+    public void notEquals_nonMatchingTitle() {
+        String title = "foo";
+        Action action1 = Action.builder().setOnClickListener(() -> {
+        }).setTitle(title).build();
+        Action action2 = Action.builder().setOnClickListener(() -> {
+        }).setTitle("not foo").build();
+
+        assertThat(action2).isNotEqualTo(action1);
+    }
+
+    @Test
+    public void notEquals_nonMatchingIcon() {
+        String title = "foo";
+        CarIcon icon1 = CarIcon.ALERT;
+        CarIcon icon2 = CarIcon.APP_ICON;
+
+        Action action1 =
+                Action.builder().setOnClickListener(() -> {
+                }).setTitle(title).setIcon(icon1).build();
+        Action action2 =
+                Action.builder().setOnClickListener(() -> {
+                }).setTitle(title).setIcon(icon2).build();
+
+        assertThat(action2).isNotEqualTo(action1);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconSpanTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconSpanTest.java
new file mode 100644
index 0000000..2439237
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconSpanTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link CarIconSpan}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CarIconSpanTest {
+    private IconCompat mIcon;
+
+    @Before
+    public void setup() {
+        mIcon =
+                IconCompat.createWithResource(
+                        ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+    }
+
+    @Test
+    public void constructor() {
+        CarIcon carIcon = CarIcon.of(mIcon);
+        CarIconSpan span = CarIconSpan.create(carIcon);
+
+        assertThat(span.getIcon()).isEqualTo(carIcon);
+    }
+
+    @Test
+    public void constructor_invalidCarIcon_throws() {
+        Uri.Builder builder = new Uri.Builder();
+        builder.scheme(ContentResolver.SCHEME_CONTENT);
+        builder.appendPath("foo/bar");
+        Uri iconUri = builder.build();
+        CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+        assertThrows(IllegalArgumentException.class, () -> CarIconSpan.create(carIcon));
+    }
+
+    @Test
+    public void equals() {
+        CarIcon carIcon = CarIcon.of(mIcon);
+        CarIconSpan span1 = CarIconSpan.create(carIcon);
+        CarIconSpan span2 = CarIconSpan.create(carIcon);
+
+        assertThat(span2).isEqualTo(span1);
+    }
+
+    @Test
+    public void notEquals() {
+        CarIcon carIcon = CarIcon.of(mIcon);
+        CarIconSpan span1 = CarIconSpan.create(carIcon);
+        CarIconSpan span2 = CarIconSpan.create(CarIcon.ALERT);
+
+        assertThat(span2).isNotEqualTo(span1);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconTest.java
new file mode 100644
index 0000000..1152c7e
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.car.app.model.CarColor.BLUE;
+import static androidx.car.app.model.CarColor.DEFAULT;
+import static androidx.car.app.model.CarColor.GREEN;
+import static androidx.car.app.model.CarIcon.BACK;
+import static androidx.car.app.model.CarIcon.TYPE_BACK;
+import static androidx.car.app.model.CarIcon.TYPE_CUSTOM;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+/** Tests for {@link CarIcon}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CarIconTest {
+    private IconCompat mIcon;
+
+    @Before
+    public void setup() {
+        mIcon =
+                IconCompat.createWithResource(
+                        ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+    }
+
+    @Test
+    public void of() {
+        CarIcon carIcon = CarIcon.of(mIcon);
+
+        assertThat(carIcon.getType()).isEqualTo(TYPE_CUSTOM);
+        assertThat(carIcon.getTint()).isNull();
+        assertThat(carIcon.getIcon()).isEqualTo(mIcon);
+    }
+
+    @Test
+    public void build_withTint() {
+        CarIcon carIcon = CarIcon.builder(mIcon).setTint(BLUE).build();
+
+        assertThat(carIcon.getType()).isEqualTo(TYPE_CUSTOM);
+        assertThat(carIcon.getTint()).isEqualTo(BLUE);
+        assertThat(carIcon.getIcon()).isEqualTo(mIcon);
+    }
+
+    @Test
+    public void build_noTint() {
+        CarIcon carIcon = CarIcon.builder(mIcon).build();
+
+        assertThat(carIcon.getType()).isEqualTo(TYPE_CUSTOM);
+        assertThat(carIcon.getTint()).isNull();
+        assertThat(mIcon).isEqualTo(carIcon.getIcon());
+    }
+
+    @Test
+    public void newBuilder_fromStandard() {
+        CarIcon carIcon = BACK.newBuilder().setTint(GREEN).build();
+
+        assertThat(carIcon.getType()).isEqualTo(TYPE_BACK);
+        assertThat(carIcon.getTint()).isEqualTo(GREEN);
+        assertThat(carIcon.getIcon()).isEqualTo(BACK.getIcon());
+    }
+
+    @Test
+    public void standard_defaultTint() {
+        assertThat(BACK.getTint()).isEqualTo(DEFAULT);
+    }
+
+    // TODO(shiufai): Add content uri equality test once we support content URI.
+    @Test
+    public void icon_from_uri() {
+        Uri.Builder builder = new Uri.Builder();
+        builder.scheme(ContentResolver.SCHEME_CONTENT);
+        builder.appendPath("foo/bar");
+        Uri iconUri = builder.build();
+
+        CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+
+        assertThat(carIcon.getType()).isEqualTo(TYPE_CUSTOM);
+        assertThat(carIcon.getTint()).isNull();
+        assertThat(carIcon.getIcon().getType()).isEqualTo(IconCompat.TYPE_URI);
+    }
+
+    @Test
+    public void custom_icon_unsupported_scheme() {
+        // Create an icon URI with "file://" scheme.
+        Uri iconUri = Uri.fromFile(new File("foo/bar"));
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> CarIcon.builder(IconCompat.createWithContentUri(iconUri)));
+    }
+
+    @Test
+    public void custom_icon_unsupported_types() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> CarIcon.builder(IconCompat.createWithAdaptiveBitmapContentUri("foo/bar")));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> CarIcon.builder(IconCompat.createWithData(new byte[0], 1, 1)));
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        CarIcon.builder(
+                                IconCompat.createWithAdaptiveBitmap(
+                                        Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8))));
+    }
+
+    @Test
+    public void equals() {
+        assertThat(BACK.equals(BACK)).isTrue();
+        CarIcon carIcon = CarIcon.of(mIcon);
+
+        assertThat(
+                CarIcon.of(
+                        IconCompat.createWithResource(
+                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1)))
+                .isEqualTo(carIcon);
+    }
+
+    @Test
+    public void notEquals() {
+        assertThat(BACK.newBuilder().setTint(GREEN).build()).isNotEqualTo(BACK);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DateTimeWithZoneTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/DateTimeWithZoneTest.java
new file mode 100644
index 0000000..1844d51
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/DateTimeWithZoneTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.car.app.TestUtils.assertDateTimeWithZoneEquals;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+/** Tests for {@link DateTimeWithZone}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DateTimeWithZoneTest {
+    @Test
+    @SuppressWarnings("JdkObsolete")
+    public void create() {
+        GregorianCalendar calendar = new GregorianCalendar(2020, 4, 15, 2, 57, 0);
+        Date date = calendar.getTime();
+        TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+        long timeSinceEpochMillis = date.getTime();
+        long timeZoneOffsetSeconds = MILLISECONDS.toSeconds(
+                timeZone.getOffset(timeSinceEpochMillis));
+        String zoneShortName = "PST";
+
+        DateTimeWithZone dateTimeWithZone =
+                DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+                        zoneShortName);
+
+        assertThat(dateTimeWithZone.getTimeSinceEpochMillis()).isEqualTo(timeSinceEpochMillis);
+        assertThat(dateTimeWithZone.getZoneOffsetSeconds()).isEqualTo(timeZoneOffsetSeconds);
+        assertThat(dateTimeWithZone.getZoneShortName()).isEqualTo(zoneShortName);
+    }
+
+    @Test
+    public void create_argumentChecks() {
+        TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+
+        long timeSinceEpochMillis = 123;
+        long timeZoneOffsetSeconds = MILLISECONDS.toSeconds(
+                timeZone.getOffset(timeSinceEpochMillis));
+        String zoneShortName = "PST";
+
+        // Negative time.
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    DateTimeWithZone.create(-1, (int) timeZoneOffsetSeconds, zoneShortName);
+                });
+
+        // Offset out of range.
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    DateTimeWithZone.create(timeSinceEpochMillis, 18 * 60 * 60 + 1, zoneShortName);
+                });
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    DateTimeWithZone.create(timeSinceEpochMillis, -18 * 60 * 60 - 1, zoneShortName);
+                });
+
+        // Null short name.
+        assertThrows(
+                NullPointerException.class,
+                () -> {
+                    DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+                            null);
+                });
+
+        // Empty short name.
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds, "");
+                });
+    }
+
+    @Test
+    @SuppressWarnings("JdkObsolete")
+    public void create_withTimeZone() {
+        GregorianCalendar calendar = new GregorianCalendar(2020, 4, 15, 2, 57, 0);
+        Date date = calendar.getTime();
+        TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+        long timeSinceEpochMillis = date.getTime();
+
+        DateTimeWithZone dateTimeWithZone = DateTimeWithZone.create(timeSinceEpochMillis, timeZone);
+
+        long timeZoneOffsetSeconds = MILLISECONDS.toSeconds(
+                timeZone.getOffset(timeSinceEpochMillis));
+        String zoneShortName = "PST";
+
+        assertThat(dateTimeWithZone.getZoneOffsetSeconds()).isEqualTo(timeZoneOffsetSeconds);
+        assertThat(dateTimeWithZone.getTimeSinceEpochMillis()).isEqualTo(timeSinceEpochMillis);
+        assertThat(dateTimeWithZone.getZoneShortName()).isEqualTo(zoneShortName);
+    }
+
+    @Test
+    public void create_withTimeZone_argumentChecks() {
+        TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+
+        // Negative time.
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    DateTimeWithZone.create(-1, timeZone);
+                });
+
+        // Null time zone.
+        assertThrows(
+                NullPointerException.class,
+                () -> {
+                    DateTimeWithZone.create(123, null);
+                });
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 26)
+    public void create_withZonedDateTime() {
+        ZonedDateTime zonedDateTime = ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]");
+        DateTimeWithZone dateTimeWithZone = DateTimeWithZone.create(zonedDateTime);
+
+        assertDateTimeWithZoneEquals(zonedDateTime, dateTimeWithZone);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 26)
+    public void create_withZonedDateTime_argumentChecks() {
+        // Null date time.
+        assertThrows(
+                NullPointerException.class,
+                () -> {
+                    DateTimeWithZone.create(null);
+                });
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 26)
+    public void equals() {
+        TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+        long timeSinceEpochMillis = System.currentTimeMillis();
+        long timeZoneOffsetSeconds =
+                Duration.ofMillis(timeZone.getOffset(timeSinceEpochMillis)).getSeconds();
+        String zoneShortName = "PST";
+
+        DateTimeWithZone dateTimeWithZone =
+                DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+                        zoneShortName);
+
+        assertThat(dateTimeWithZone)
+                .isEqualTo(
+                        DateTimeWithZone.create(
+                                timeSinceEpochMillis, (int) timeZoneOffsetSeconds, zoneShortName));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 26)
+    public void notEquals_differentTimeSinceEpoch() {
+        TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+        long timeSinceEpochMillis = System.currentTimeMillis();
+        long timeZoneOffsetSeconds =
+                Duration.ofMillis(timeZone.getOffset(timeSinceEpochMillis)).getSeconds();
+        String zoneShortName = "PST";
+
+        DateTimeWithZone dateTimeWithZone =
+                DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+                        zoneShortName);
+
+        assertThat(dateTimeWithZone)
+                .isNotEqualTo(
+                        DateTimeWithZone.create(
+                                timeSinceEpochMillis + 1, (int) timeZoneOffsetSeconds,
+                                zoneShortName));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 26)
+    public void notEquals_differentTimeZoneOffsetSeconds() {
+        TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+        long timeSinceEpochMillis = System.currentTimeMillis();
+        long timeZoneOffsetSeconds =
+                Duration.ofMillis(timeZone.getOffset(timeSinceEpochMillis)).getSeconds();
+        String zoneShortName = "PST";
+
+        DateTimeWithZone dateTimeWithZone =
+                DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+                        zoneShortName);
+
+        assertThat(dateTimeWithZone)
+                .isNotEqualTo(
+                        DateTimeWithZone.create(
+                                timeSinceEpochMillis, (int) timeZoneOffsetSeconds + 1,
+                                zoneShortName));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 26)
+    public void notEquals_differentTimeZone() {
+        TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
+        long timeSinceEpochMillis = System.currentTimeMillis();
+        long timeZoneOffsetSeconds =
+                Duration.ofMillis(timeZone.getOffset(timeSinceEpochMillis)).getSeconds();
+        String zoneShortName = "PST";
+
+        DateTimeWithZone dateTimeWithZone =
+                DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+                        zoneShortName);
+
+        assertThat(dateTimeWithZone)
+                .isNotEqualTo(
+                        DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+                                "UTC"));
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceSpanTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceSpanTest.java
new file mode 100644
index 0000000..a05bed7
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceSpanTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link DistanceSpan}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DistanceSpanTest {
+    private final Distance mDistance =
+            Distance.create(/* displayDistance= */ 10, Distance.UNIT_KILOMETERS);
+
+    @Test
+    public void constructor() {
+        DistanceSpan span = DistanceSpan.create(mDistance);
+        assertThat(span.getDistance()).isEqualTo(mDistance);
+    }
+
+    @Test
+    public void equals() {
+        DistanceSpan span = DistanceSpan.create(mDistance);
+        assertThat(span).isEqualTo(DistanceSpan.create(mDistance));
+    }
+
+    @Test
+    public void notEquals() {
+        DistanceSpan span = DistanceSpan.create(mDistance);
+        assertThat(span)
+                .isNotEqualTo(
+                        DistanceSpan.create(
+                                Distance.create(/* displayDistance= */ 200, Distance.UNIT_METERS)));
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceTest.java
new file mode 100644
index 0000000..91c7028
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceTest.java
@@ -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.
+ */
+
+package androidx.car.app.model;
+
+import static androidx.car.app.model.Distance.UNIT_KILOMETERS;
+import static androidx.car.app.model.Distance.UNIT_METERS;
+import static androidx.car.app.model.Distance.UNIT_YARDS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Distance}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DistanceTest {
+
+    private static final double DISPLAY_DISTANCE = 1.2d;
+    private static final int DISPLAY_UNIT = UNIT_KILOMETERS;
+    private static final double DELTA = 0.00001;
+
+    @Test
+    public void createInstance_negativeMeter() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Distance.create(/* displayDistance= */ -1, UNIT_METERS));
+    }
+
+    @Test
+    public void createInstance() {
+        Distance distance = Distance.create(DISPLAY_DISTANCE, DISPLAY_UNIT);
+        assertThat(distance.getDisplayDistance()).isWithin(DELTA).of(DISPLAY_DISTANCE);
+        assertThat(distance.getDisplayUnit()).isEqualTo(DISPLAY_UNIT);
+    }
+
+    @Test
+    public void equals() {
+        Distance distance = Distance.create(DISPLAY_DISTANCE, DISPLAY_UNIT);
+
+        assertThat(Distance.create(DISPLAY_DISTANCE, DISPLAY_UNIT)).isEqualTo(distance);
+    }
+
+    @Test
+    public void notEquals_differentDisplayValue() {
+        Distance distance = Distance.create(DISPLAY_DISTANCE, DISPLAY_UNIT);
+
+        assertThat(Distance.create(DISPLAY_DISTANCE + 1, DISPLAY_UNIT)).isNotEqualTo(distance);
+    }
+
+    @Test
+    public void notEquals_differentDisplayUnit() {
+        Distance distance = Distance.create(DISPLAY_DISTANCE, DISPLAY_UNIT);
+
+        assertThat(Distance.create(DISPLAY_DISTANCE, UNIT_YARDS)).isNotEqualTo(distance);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DurationSpanTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/DurationSpanTest.java
new file mode 100644
index 0000000..e43f2e1
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/DurationSpanTest.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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link DurationSpan}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DurationSpanTest {
+    @Test
+    public void constructor() {
+        DurationSpan span = DurationSpan.create(1);
+        assertThat(span.getDurationSeconds()).isEqualTo(1);
+    }
+
+    @Test
+    public void equals() {
+        DurationSpan span = DurationSpan.create(1);
+        assertThat(span).isEqualTo(DurationSpan.create(1));
+    }
+
+    @Test
+    public void notEquals() {
+        DurationSpan span = DurationSpan.create(1);
+        assertThat(span).isNotEqualTo(DurationSpan.create(2));
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ForegroundCarColorSpanTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
new file mode 100644
index 0000000..4f320f0
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.car.app.model.CarColor.BLUE;
+import static androidx.car.app.model.CarColor.GREEN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link CarIconSpan}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ForegroundCarColorSpanTest {
+    @Test
+    public void constructor() {
+        ForegroundCarColorSpan span = ForegroundCarColorSpan.create(BLUE);
+
+        assertThat(span.getColor()).isEqualTo(BLUE);
+    }
+
+    @Test
+    public void equals() {
+        ForegroundCarColorSpan span = ForegroundCarColorSpan.create(BLUE);
+        assertThat(ForegroundCarColorSpan.create(BLUE)).isEqualTo(span);
+    }
+
+    @Test
+    public void notEquals() {
+        ForegroundCarColorSpan span = ForegroundCarColorSpan.create(BLUE);
+        assertThat(ForegroundCarColorSpan.create(GREEN)).isNotEqualTo(span);
+    }
+
+    @Test
+    public void customColorDisallowed() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ForegroundCarColorSpan.create(CarColor.createCustom(0xdead, 0xbeef)));
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/GridItemTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/GridItemTest.java
new file mode 100644
index 0000000..7fe88b4
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/GridItemTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.car.app.model.CarIcon.ALERT;
+import static androidx.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link GridItem}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class GridItemTest {
+
+    @Test
+    public void create_defaultValues() {
+        GridItem gridItem = GridItem.builder().setImage(BACK).build();
+
+        assertThat(BACK).isEqualTo(gridItem.getImage());
+        assertThat(gridItem.getImageType()).isEqualTo(GridItem.IMAGE_TYPE_LARGE);
+        assertThat(gridItem.getTitle()).isNull();
+        assertThat(gridItem.getText()).isNull();
+    }
+
+    @Test
+    public void title_charSequence() {
+        String title = "foo";
+        GridItem gridItem = GridItem.builder().setTitle(title).setImage(BACK).build();
+
+        assertThat(CarText.create(title)).isEqualTo(gridItem.getTitle());
+    }
+
+    @Test
+    public void text_charSequence() {
+        String text = "foo";
+        GridItem gridItem = GridItem.builder().setTitle("title").setText(text).setImage(
+                BACK).build();
+
+        assertThat(CarText.create(text)).isEqualTo(gridItem.getText());
+    }
+
+    @Test
+    public void textWithoutTitle_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> GridItem.builder().setText("text").setImage(BACK).build());
+    }
+
+    @Test
+    public void create_noImage_throwsException() {
+        assertThrows(IllegalStateException.class, () -> GridItem.builder().setTitle("foo").build());
+    }
+
+    @Test
+    public void equals() {
+        String title = "title";
+        String text = "text";
+        GridItem gridItem = GridItem.builder().setTitle(title).setText(text).setImage(BACK).build();
+
+        assertThat(GridItem.builder().setTitle(title).setText(text).setImage(BACK).build())
+                .isEqualTo(gridItem);
+    }
+
+    @Test
+    public void notEquals_differentTitle() {
+        String title = "title";
+        GridItem gridItem = GridItem.builder().setTitle(title).setImage(BACK).build();
+
+        assertThat(GridItem.builder().setTitle("foo").setImage(BACK).build()).isNotEqualTo(
+                gridItem);
+    }
+
+    @Test
+    public void notEquals_differentText() {
+        String title = "title";
+        String text = "text";
+        GridItem gridItem = GridItem.builder().setTitle(title).setText(text).setImage(BACK).build();
+
+        assertThat(GridItem.builder().setTitle(title).setText("foo").setImage(BACK).build())
+                .isNotEqualTo(gridItem);
+    }
+
+    @Test
+    public void notEquals_differentImage() {
+        GridItem gridItem = GridItem.builder().setImage(BACK).build();
+
+        assertThat(GridItem.builder().setImage(ALERT).build()).isNotEqualTo(gridItem);
+    }
+
+    @Test
+    public void notEquals_differentToggle() {
+        Toggle toggle1 = Toggle.builder(isChecked -> {
+        }).setChecked(true).build();
+        Toggle toggle2 = Toggle.builder(isChecked -> {
+        }).setChecked(false).build();
+        GridItem gridItem = GridItem.builder().setImage(BACK).setToggle(toggle1).build();
+
+        assertThat(GridItem.builder().setImage(BACK).setToggle(toggle2).build()).isNotEqualTo(
+                gridItem);
+    }
+
+// TODO(shiufai): revisit the following as the test is not running on the main looper thread, and
+//  thus the verify is failing.
+//    @Test
+//    public void clickListener() throws RemoteException {
+//        OnClickListener >
+//        GridItem gridItem =
+//                GridItem.builder().setImage(BACK).setOnClickListener(onClickListener).build();
+//        gridItem.getOnClickListener().getListener().onClick(mock(IOnDoneCallback.class));
+//        verify(onClickListener).onClick();
+//    }
+
+    @Test
+    public void setToggle() {
+        Toggle toggle = Toggle.builder(isChecked -> {
+        }).build();
+        GridItem gridItem = GridItem.builder().setImage(BACK).setToggle(toggle).build();
+        assertThat(toggle).isEqualTo(gridItem.getToggle());
+    }
+
+    @Test
+    public void setOnClickListenerAndToggle_throws() {
+        Toggle toggle = Toggle.builder(isChecked -> {
+        }).build();
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        GridItem.builder()
+                                .setImage(BACK)
+                                .setOnClickListener(() -> {
+                                })
+                                .setToggle(toggle)
+                                .build());
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java
new file mode 100644
index 0000000..ef3832b
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java
@@ -0,0 +1,373 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.car.app.model.CarIcon.ALERT;
+import static androidx.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.utils.Logger;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link GridTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class GridTemplateTest {
+    private final Logger mLogger = message -> {
+    };
+
+    @Test
+    public void createInstance_emptyList_notLoading_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> GridTemplate.builder().setTitle("Title").build());
+
+        // Positive case
+        GridTemplate.builder().setTitle("Title").setLoading(true).build();
+    }
+
+    @Test
+    public void createInstance_isLoading_hasList_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        GridTemplate.builder()
+                                .setTitle("Title")
+                                .setLoading(true)
+                                .setSingleList(TestUtils.getGridItemList(2))
+                                .build());
+    }
+
+    @Test
+    public void createInstance_noHeaderTitleOrAction_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> GridTemplate.builder().setSingleList(TestUtils.getGridItemList(2)).build());
+
+        // Positive cases.
+        GridTemplate.builder().setTitle("Title").setSingleList(
+                TestUtils.getGridItemList(2)).build();
+        GridTemplate.builder()
+                .setHeaderAction(Action.BACK)
+                .setSingleList(TestUtils.getGridItemList(2))
+                .build();
+    }
+
+    @Test
+    public void createInstance_setSingleList() {
+        ItemList list = TestUtils.getGridItemList(2);
+        GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+        assertThat(template.getSingleList()).isEqualTo(list);
+    }
+
+    @Test
+    public void createInstance_setHeaderAction_invalidActionThrows() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        GridTemplate.builder()
+                                .setHeaderAction(
+                                        Action.builder().setTitle("Action").setOnClickListener(
+                                                () -> {
+                                                }).build()));
+    }
+
+    @Test
+    public void createInstance_setHeaderAction() {
+        GridTemplate template =
+                GridTemplate.builder()
+                        .setSingleList(TestUtils.getGridItemList(2))
+                        .setHeaderAction(Action.BACK)
+                        .build();
+        assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+    }
+
+    @Test
+    public void createInstance_setActionStrip() {
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        GridTemplate template =
+                GridTemplate.builder()
+                        .setSingleList(TestUtils.getGridItemList(2))
+                        .setTitle("Title")
+                        .setActionStrip(actionStrip)
+                        .build();
+        assertThat(template.getActionStrip()).isEqualTo(actionStrip);
+    }
+
+    @Test
+    public void createInstance_setBackground() {
+        GridTemplate template =
+                GridTemplate.builder()
+                        .setTitle("Title")
+                        .setLoading(true)
+                        .setBackgroundImage(BACK)
+                        .build();
+        assertThat(template.getBackgroundImage()).isEqualTo(BACK);
+    }
+
+    @Test
+    public void validate_fromLoadingState_isRefresh() {
+        GridItem.Builder gridItem = GridItem.builder().setImage(BACK);
+        ItemList list = ItemList.builder().addItem(gridItem.build()).build();
+        GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+
+        // Going from loading state to new content is allowed.
+        assertThat(
+                template.isRefresh(
+                        GridTemplate.builder().setTitle("Title").setLoading(true).build(),
+                        mLogger))
+                .isTrue();
+    }
+
+    @Test
+    public void validate_mutableProperties_isRefresh() {
+        GridItem.Builder gridItem = GridItem.builder().setImage(BACK);
+        ItemList list = ItemList.builder().addItem(gridItem.build()).build();
+        GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+
+        // Ensure a template is a refresh of itself.
+        assertThat(template.isRefresh(template, mLogger)).isTrue();
+
+        // Allowed mutable states.
+        assertThat(
+                template.isRefresh(
+                        GridTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(
+                                        ItemList.builder()
+                                                .addItem(gridItem.setOnClickListener(() -> {
+                                                }).setImage(BACK).build())
+                                                .build())
+                                .setHeaderAction(Action.BACK)
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
+                                .build(),
+                        mLogger))
+                .isTrue();
+    }
+
+    @Test
+    public void validate_titleUpdate_isNotRefresh() {
+        ItemList list = ItemList.builder().build();
+        GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+
+        // Title updates are disallowed.
+        assertThat(
+                template.isRefresh(
+                        GridTemplate.builder().setSingleList(list).setTitle("Title2").build(),
+                        mLogger))
+                .isFalse();
+    }
+
+    @Test
+    public void validate_gridItemImageAndTextUpdates_isNotRefresh() {
+        GridItem.Builder gridItem =
+                GridItem.builder().setImage(BACK).setTitle("Title1").setText("Text1");
+        ItemList list = ItemList.builder().addItem(gridItem.build()).build();
+        GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+
+        // Ensure a template is a refresh of itself.
+        assertThat(template.isRefresh(template, mLogger)).isTrue();
+
+        // Image updates are disallowed.
+        assertThat(
+                template.isRefresh(
+                        GridTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(
+                                        ItemList.builder().addItem(
+                                                gridItem.setImage(ALERT).build()).build())
+                                .build(),
+                        mLogger))
+                .isFalse();
+
+        // Text updates are disallowed
+        assertThat(
+                template.isRefresh(
+                        GridTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(
+                                        ItemList.builder().addItem(
+                                                gridItem.setTitle("Title2").build()).build())
+                                .build(),
+                        mLogger))
+                .isFalse();
+        assertThat(
+                template.isRefresh(
+                        GridTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(
+                                        ItemList.builder().addItem(
+                                                gridItem.setText("Text2").build()).build())
+                                .build(),
+                        mLogger))
+                .isFalse();
+    }
+
+    @Test
+    public void validate_newGridItem_isNotRefresh() {
+        GridItem.Builder gridItem = GridItem.builder().setImage(BACK);
+        ItemList list = ItemList.builder().addItem(gridItem.build()).build();
+        GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+
+        // Additional grid items are disallowed.
+        assertThat(
+                template.isRefresh(
+                        GridTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(
+                                        ItemList.builder()
+                                                .addItem(gridItem.build())
+                                                .addItem(gridItem.build())
+                                                .build())
+                                .build(),
+                        mLogger))
+                .isFalse();
+    }
+
+    @Test
+    public void validate_toLoadingState_isNotRefresh() {
+        // Going from content to loading state is disallowed.
+        assertThat(
+                GridTemplate.builder()
+                        .setTitle("Title")
+                        .setLoading(true)
+                        .build()
+                        .isRefresh(
+                                GridTemplate.builder()
+                                        .setTitle("Title")
+                                        .setSingleList(ItemList.builder().build())
+                                        .build(),
+                                mLogger))
+                .isFalse();
+    }
+
+    @Test
+    public void resetList_clearsSingleList() {
+        GridTemplate.Builder builder =
+                GridTemplate.builder()
+                        .setSingleList(TestUtils.getGridItemList(2))
+                        .setHeaderAction(Action.BACK);
+
+        assertThrows(IllegalStateException.class, () -> builder.clearAllLists().build());
+    }
+
+    @Test
+    public void equals() {
+        ItemList itemList = ItemList.builder().build();
+        String title = "title";
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+
+        GridTemplate template =
+                GridTemplate.builder()
+                        .setSingleList(itemList)
+                        .setHeaderAction(Action.BACK)
+                        .setActionStrip(actionStrip)
+                        .setTitle(title)
+                        .build();
+
+        assertThat(template)
+                .isEqualTo(
+                        GridTemplate.builder()
+                                .setSingleList(itemList)
+                                .setHeaderAction(Action.BACK)
+                                .setActionStrip(actionStrip)
+                                .setTitle(title)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentItemList() {
+        ItemList itemList = ItemList.builder().build();
+
+        GridTemplate template =
+                GridTemplate.builder().setTitle("Title").setSingleList(itemList).build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        GridTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(
+                                        ItemList.builder().addItem(
+                                                GridItem.builder().setImage(BACK).build()).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentHeaderAction() {
+        ItemList itemList = ItemList.builder().build();
+
+        GridTemplate template =
+                GridTemplate.builder().setSingleList(itemList).setHeaderAction(Action.BACK).build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        GridTemplate.builder()
+                                .setSingleList(itemList)
+                                .setHeaderAction(Action.APP_ICON)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentTitle() {
+        ItemList itemList = ItemList.builder().build();
+        String title = "title";
+
+        GridTemplate template = GridTemplate.builder().setSingleList(itemList).setTitle(
+                title).build();
+
+        assertThat(template)
+                .isNotEqualTo(GridTemplate.builder().setSingleList(itemList).setTitle(
+                        "foo").build());
+    }
+
+    @Test
+    public void notEquals_differentActionStrip() {
+        ItemList itemList = ItemList.builder().build();
+        String title = "title";
+
+        GridTemplate template =
+                GridTemplate.builder()
+                        .setSingleList(itemList)
+                        .setTitle(title)
+                        .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        GridTemplate.builder()
+                                .setSingleList(itemList)
+                                .setTitle(title)
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
+                                .build());
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java
new file mode 100644
index 0000000..056128a
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java
@@ -0,0 +1,571 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.car.app.model.CarIcon.ALERT;
+import static androidx.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+
+import android.os.RemoteException;
+import android.text.SpannableString;
+
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.model.ItemList.OnItemVisibilityChangedListener;
+import androidx.car.app.model.ItemList.OnSelectedListener;
+import androidx.car.app.utils.Logger;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Collections;
+
+/** Tests for {@link ItemListTest}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ItemListTest {
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private IOnDoneCallback.Stub mMockOnDoneCallback;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void createEmpty() {
+        ItemList list = ItemList.builder().build();
+        assertThat(list.getItems()).isEqualTo(Collections.emptyList());
+    }
+
+    @Test
+    public void createRows() {
+        Row row1 = Row.builder().setTitle("Row1").build();
+        Row row2 = Row.builder().setTitle("Row2").build();
+        ItemList list = ItemList.builder().addItem(row1).addItem(row2).build();
+
+        assertThat(list.getItems()).hasSize(2);
+        assertThat(list.getItems().get(0)).isEqualTo(row1);
+        assertThat(list.getItems().get(1)).isEqualTo(row2);
+    }
+
+    @Test
+    public void createGridItems() {
+        GridItem gridItem1 = GridItem.builder().setImage(BACK).build();
+        GridItem gridItem2 = GridItem.builder().setImage(BACK).build();
+        ItemList list = ItemList.builder().addItem(gridItem1).addItem(gridItem2).build();
+
+        assertThat(list.getItems()).containsExactly(gridItem1, gridItem2).inOrder();
+    }
+
+    @Test
+    public void clearRows() {
+        Row row1 = Row.builder().setTitle("Row1").build();
+        Row row2 = Row.builder().setTitle("Row2").build();
+        ItemList list = ItemList.builder().addItem(row1).addItem(row2).clearItems().build();
+
+        assertThat(list.getItems()).isEmpty();
+    }
+
+    @Test
+    public void clearGridItems() {
+        GridItem gridItem1 = GridItem.builder().setImage(BACK).build();
+        GridItem gridItem2 = GridItem.builder().setImage(BACK).build();
+        ItemList list = ItemList.builder().addItem(gridItem1).addItem(
+                gridItem2).clearItems().build();
+
+        assertThat(list.getItems()).isEmpty();
+    }
+
+    @Test
+    public void setSelectedable_emptyList_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> ItemList.builder().setSelectable(selectedIndex -> {
+                }).build());
+    }
+
+    @Test
+    public void setSelectedIndex_greaterThanListSize_throws() {
+        Row row1 = Row.builder().setTitle("Row1").build();
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        ItemList.builder()
+                                .addItem(row1)
+                                .setSelectable(selectedIndex -> {
+                                })
+                                .setSelectedIndex(2)
+                                .build());
+    }
+
+    @Test
+    public void setSelectable() throws RemoteException {
+        OnSelectedListener mockListener = mock(OnSelectedListener.class);
+        ItemList itemList =
+                ItemList.builder()
+                        .addItem(Row.builder().setTitle("title").build())
+                        .setSelectable(mockListener)
+                        .build();
+
+        // TODO(shiufai): revisit the following as the test is not running on the main looper
+        //  thread, and thus the verify is failing.
+//        itemList.getOnSelectedListener().onSelected(0, mockOnDoneCallback);
+//        verify(mockListener).onSelected(eq(0));
+    }
+
+    @Test
+    public void setSelectable_disallowOnClickListenerInRows() {
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        ItemList.builder()
+                                .addItem(Row.builder().setTitle("foo").setOnClickListener(() -> {
+                                }).build())
+                                .setSelectable((index) -> {
+                                })
+                                .build());
+
+        // Positive test.
+        ItemList.builder()
+                .addItem(Row.builder().setTitle("foo").build())
+                .setSelectable((index) -> {
+                })
+                .build();
+    }
+
+    @Test
+    public void setSelectable_disallowToggleInRow() {
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        ItemList.builder()
+                                .addItem(Row.builder().setToggle(Toggle.builder(isChecked -> {
+                                }).build()).build())
+                                .setSelectable((index) -> {
+                                })
+                                .build());
+    }
+
+    @Test
+    public void setOnItemVisibilityChangeListener_triggerListener() throws RemoteException {
+        OnItemVisibilityChangedListener listener = mock(OnItemVisibilityChangedListener.class);
+        ItemList list =
+                ItemList.builder()
+                        .addItem(Row.builder().setTitle("1").build())
+                        .setOnItemsVisibilityChangeListener(listener)
+                        .build();
+
+        // TODO(shiufai): revisit the following as the test is not running on the main looper
+        //  thread, and thus the verify is failing.
+//        list.getOnItemsVisibilityChangeListener().onItemVisibilityChanged(0, 1,
+//        mockOnDoneCallback);
+//        ArgumentCaptor<Integer> startIndexCaptor = ArgumentCaptor.forClass(Integer.class);
+//        ArgumentCaptor<Integer> endIndexCaptor = ArgumentCaptor.forClass(Integer.class);
+//        verify(listener).onItemVisibilityChanged(startIndexCaptor.capture(),
+//                endIndexCaptor.capture());
+//        assertThat(startIndexCaptor.getValue()).isEqualTo(0);
+//        assertThat(endIndexCaptor.getValue()).isEqualTo(1);
+    }
+
+    @Test
+    public void validateRows_isRefresh() {
+        Logger logger = message -> {
+        };
+        Row.Builder row = Row.builder().setTitle("Title1");
+        ItemList listWithRows = ItemList.builder().addItem(row.build()).build();
+
+        // Text updates are disallowed.
+        ItemList listWithDifferentTitle =
+                ItemList.builder().addItem(row.setTitle("Title2").build()).build();
+        ItemList listWithDifferentText =
+                ItemList.builder().addItem(row.addText("Text").build()).build();
+        assertThat(listWithDifferentTitle.isRefresh(listWithRows, logger)).isFalse();
+        assertThat(listWithDifferentText.isRefresh(listWithRows, logger)).isFalse();
+
+        // Additional rows are disallowed.
+        ItemList listWithTwoRows = ItemList.builder().addItem(row.build()).addItem(
+                row.build()).build();
+        assertThat(listWithTwoRows.isRefresh(listWithRows, logger)).isFalse();
+    }
+
+    @Test
+    public void validateGridItems_isRefresh() {
+        Logger logger = message -> {
+        };
+        GridItem.Builder gridItem = GridItem.builder().setImage(BACK).setTitle("Title1");
+        ItemList listWithGridItems = ItemList.builder().addItem(gridItem.build()).build();
+
+        // Text updates are disallowed.
+        ItemList listWithDifferentTitle =
+                ItemList.builder().addItem(gridItem.setTitle("Title2").build()).build();
+        ItemList listWithDifferentText =
+                ItemList.builder().addItem(gridItem.setText("Text").build()).build();
+        assertThat(listWithDifferentTitle.isRefresh(listWithGridItems, logger)).isFalse();
+        assertThat(listWithDifferentText.isRefresh(listWithGridItems, logger)).isFalse();
+
+        // Image updates are disallowed.
+        ItemList listWithDifferentImage =
+                ItemList.builder().addItem(gridItem.setImage(ALERT).build()).build();
+        assertThat(listWithDifferentImage.isRefresh(listWithGridItems, logger)).isFalse();
+
+        // Additional grid items are disallowed.
+        ItemList listWithTwoGridItems =
+                ItemList.builder().addItem(gridItem.build()).addItem(gridItem.build()).build();
+        assertThat(listWithTwoGridItems.isRefresh(listWithGridItems, logger)).isFalse();
+    }
+
+    @Test
+    public void validateRows_isRefresh_differentSpansAreIgnored() {
+        Logger logger = message -> {
+        };
+        SpannableString textWithDistanceSpan = new SpannableString("Text");
+        textWithDistanceSpan.setSpan(
+                DistanceSpan.create(Distance.create(1000, Distance.UNIT_KILOMETERS)),
+                /* start= */ 0,
+                /* end= */ 1,
+                /* flags= */ 0);
+        SpannableString textWithDurationSpan = new SpannableString("Text");
+        textWithDurationSpan.setSpan(DurationSpan.create(1), 0, /* end= */ 1, /* flags= */ 0);
+
+        ItemList list1 =
+                ItemList.builder()
+                        .addItem(
+                                Row.builder().setTitle(textWithDistanceSpan).addText(
+                                        textWithDurationSpan).build())
+                        .build();
+        ItemList list2 =
+                ItemList.builder()
+                        .addItem(
+                                Row.builder().setTitle(textWithDurationSpan).addText(
+                                        textWithDistanceSpan).build())
+                        .build();
+        ItemList list3 =
+                ItemList.builder()
+                        .addItem(Row.builder().setTitle("Text2").addText("Text2").build())
+                        .build();
+
+        assertThat(list2.isRefresh(list1, logger)).isTrue();
+        assertThat(list3.isRefresh(list1, logger)).isFalse();
+    }
+
+    @Test
+    public void validateRows_isRefresh_differentToggleStatesAllowTextUpdates() {
+        Logger logger = message -> {
+        };
+        Toggle  -> {
+        }).setChecked(true).build();
+        Toggle offToggle = Toggle.builder(isChecked -> {
+        }).setChecked(false).build();
+
+        ItemList listWithOnToggle =
+                ItemList.builder()
+                        .addItem(Row.builder().setTitle("Title1").setToggle(onToggle).build())
+                        .build();
+        ItemList listWithOffToggle =
+                ItemList.builder()
+                        .addItem(Row.builder().setTitle("Title1").setToggle(offToggle).build())
+                        .build();
+        ItemList listWithoutToggle =
+                ItemList.builder().addItem(Row.builder().setTitle("Title2").build()).build();
+        ItemList listWithOffToggleDifferentText =
+                ItemList.builder()
+                        .addItem(Row.builder().setTitle("Title2").addText("Text").setToggle(
+                                offToggle).build())
+                        .build();
+        ItemList listWithOnToggleDifferentText =
+                ItemList.builder()
+                        .addItem(Row.builder().setTitle("Title2").setToggle(onToggle).build())
+                        .build();
+
+        // Going from toggle to no toggle is not a refresh.
+        assertThat(listWithOnToggle.isRefresh(listWithoutToggle, logger)).isFalse();
+
+        // Going from on toggle to off toggle, or vice versa, is always a refresh
+        assertThat(listWithOnToggle.isRefresh(listWithOffToggleDifferentText, logger)).isTrue();
+        assertThat(listWithOffToggleDifferentText.isRefresh(listWithOnToggle, logger)).isTrue();
+
+        // If toggle state is the same, then text changes are not considered a refresh.
+        assertThat(listWithOnToggle.isRefresh(listWithOnToggleDifferentText, logger)).isFalse();
+        assertThat(listWithOffToggle.isRefresh(listWithOffToggleDifferentText, logger)).isFalse();
+    }
+
+    @Test
+    public void validateGridItems_isRefresh_differentToggleStatesAllowTextUpdates() {
+        Logger logger = message -> {
+        };
+        Toggle  -> {
+        }).setChecked(true).build();
+        Toggle offToggle = Toggle.builder(isChecked -> {
+        }).setChecked(false).build();
+
+        ItemList listWithOnToggle =
+                ItemList.builder()
+                        .addItem(
+                                GridItem.builder().setImage(BACK).setTitle("Title1").setToggle(
+                                        onToggle).build())
+                        .build();
+        ItemList listWithOffToggle =
+                ItemList.builder()
+                        .addItem(
+                                GridItem.builder().setImage(BACK).setTitle("Title1").setToggle(
+                                        offToggle).build())
+                        .build();
+        ItemList listWithoutToggle =
+                ItemList.builder()
+                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title2").build())
+                        .build();
+        ItemList listWithOffToggleDifferentText =
+                ItemList.builder()
+                        .addItem(
+                                GridItem.builder()
+                                        .setImage(BACK)
+                                        .setTitle("Title2")
+                                        .setText("Text")
+                                        .setToggle(offToggle)
+                                        .build())
+                        .build();
+        ItemList listWithOnToggleDifferentText =
+                ItemList.builder()
+                        .addItem(
+                                GridItem.builder().setImage(BACK).setTitle("Title2").setToggle(
+                                        onToggle).build())
+                        .build();
+
+        // Going from toggle to no toggle is not a refresh.
+        assertThat(listWithOnToggle.isRefresh(listWithoutToggle, logger)).isFalse();
+
+        // Going from on toggle to off toggle, or vice versa, is always a refresh
+        assertThat(listWithOnToggle.isRefresh(listWithOffToggleDifferentText, logger)).isTrue();
+        assertThat(listWithOffToggleDifferentText.isRefresh(listWithOnToggle, logger)).isTrue();
+
+        // If toggle state is the same, then text changes are not considered a refresh.
+        assertThat(listWithOnToggle.isRefresh(listWithOnToggleDifferentText, logger)).isFalse();
+        assertThat(listWithOffToggle.isRefresh(listWithOffToggleDifferentText, logger)).isFalse();
+    }
+
+    @Test
+    public void validateGridItems_isRefresh_differentToggleStatesAllowImageUpdates() {
+        Logger logger = message -> {
+        };
+        Toggle  -> {
+        }).setChecked(true).build();
+        Toggle offToggle = Toggle.builder(isChecked -> {
+        }).setChecked(false).build();
+
+        ItemList listWithOnToggle =
+                ItemList.builder()
+                        .addItem(GridItem.builder().setImage(BACK).setToggle(onToggle).build())
+                        .build();
+        ItemList listWithOffToggle =
+                ItemList.builder()
+                        .addItem(GridItem.builder().setImage(BACK).setToggle(offToggle).build())
+                        .build();
+        ItemList listWithoutToggle =
+                ItemList.builder().addItem(GridItem.builder().setImage(ALERT).build()).build();
+        ItemList listWithOffToggleDifferentImage =
+                ItemList.builder()
+                        .addItem(GridItem.builder().setImage(ALERT).setToggle(offToggle).build())
+                        .build();
+        ItemList listWithOnToggleDifferentImage =
+                ItemList.builder()
+                        .addItem(GridItem.builder().setImage(ALERT).setToggle(onToggle).build())
+                        .build();
+
+        // Going from toggle to no toggle is not a refresh.
+        assertThat(listWithOnToggle.isRefresh(listWithoutToggle, logger)).isFalse();
+
+        // Going from on toggle to off toggle, or vice versa, is always a refresh
+        assertThat(listWithOnToggle.isRefresh(listWithOffToggleDifferentImage, logger)).isTrue();
+        assertThat(listWithOffToggleDifferentImage.isRefresh(listWithOnToggle, logger)).isTrue();
+
+        // If toggle state is the same, then image changes are not considered a refresh.
+        assertThat(listWithOnToggle.isRefresh(listWithOnToggleDifferentImage, logger)).isFalse();
+        assertThat(listWithOffToggle.isRefresh(listWithOffToggleDifferentImage, logger)).isFalse();
+    }
+
+    @Test
+    public void validateGridItems_isRefresh_differentSelectedIndexAllowTextUpdates() {
+        Logger logger = message -> {
+        };
+        OnSelectedListener >
+
+        ItemList listWithItem0Selected =
+                ItemList.builder()
+                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title11").build())
+                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title12").build())
+                        .setSelectable(onSelectedListener)
+                        .setSelectedIndex(0)
+                        .build();
+        ItemList listWithItem1Selected =
+                ItemList.builder()
+                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title21").build())
+                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title22").build())
+                        .setSelectable(onSelectedListener)
+                        .setSelectedIndex(1)
+                        .build();
+        ItemList listWithItem0SelectedDifferentText =
+                ItemList.builder()
+                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title21").build())
+                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title22").build())
+                        .setSelectable(onSelectedListener)
+                        .setSelectedIndex(0)
+                        .build();
+        ItemList listWithoutOnSelectedListener =
+                ItemList.builder()
+                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title21").build())
+                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title22").build())
+                        .build();
+
+        // Selecting item 1 from item 0, or vice versa, is always a refresh.
+        assertThat(listWithItem0Selected.isRefresh(listWithItem1Selected, logger)).isTrue();
+        assertThat(listWithItem1Selected.isRefresh(listWithItem0Selected, logger)).isTrue();
+
+        // If item selection is the same, it is not considered a refresh
+        assertThat(listWithItem0Selected.isRefresh(listWithItem0SelectedDifferentText, logger))
+                .isFalse();
+
+        // If one of the ItemList doesn't have a selectable state, it is not a refresh.
+        assertThat(
+                listWithItem0Selected.isRefresh(listWithoutOnSelectedListener, logger)).isFalse();
+    }
+
+    @Test
+    public void equals_itemListWithRows() {
+        Row row = Row.builder().setTitle("Title").build();
+        ItemList itemList =
+                ItemList.builder()
+                        .setSelectable((index) -> {
+                        })
+                        .setNoItemsMessage("no items")
+                        .setSelectedIndex(0)
+                        .setOnItemsVisibilityChangeListener((start, end) -> {
+                        })
+                        .addItem(row)
+                        .build();
+        assertThat(itemList)
+                .isEqualTo(
+                        ItemList.builder()
+                                .setSelectable((index) -> {
+                                })
+                                .setNoItemsMessage("no items")
+                                .setSelectedIndex(0)
+                                .setOnItemsVisibilityChangeListener((start, end) -> {
+                                })
+                                .addItem(row)
+                                .build());
+    }
+
+    @Test
+    public void equals_itemListWithGridItems() {
+        GridItem gridItem = GridItem.builder().setImage(BACK).setTitle("Title").build();
+        ItemList itemList =
+                ItemList.builder()
+                        .setSelectable((index) -> {
+                        })
+                        .setNoItemsMessage("no items")
+                        .setSelectedIndex(0)
+                        .setOnItemsVisibilityChangeListener((start, end) -> {
+                        })
+                        .addItem(gridItem)
+                        .build();
+        assertThat(itemList)
+                .isEqualTo(
+                        ItemList.builder()
+                                .setSelectable((index) -> {
+                                })
+                                .setNoItemsMessage("no items")
+                                .setSelectedIndex(0)
+                                .setOnItemsVisibilityChangeListener((start, end) -> {
+                                })
+                                .addItem(gridItem)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentNoItemsMessage() {
+        ItemList itemList = ItemList.builder().setNoItemsMessage("no items").build();
+        assertThat(itemList).isNotEqualTo(ItemList.builder().setNoItemsMessage("YO").build());
+    }
+
+    @Test
+    public void notEquals_differentSelectedIndex() {
+        Row row = Row.builder().setTitle("Title").build();
+        ItemList itemList =
+                ItemList.builder().setSelectable((index) -> {
+                }).addItem(row).addItem(row).build();
+        assertThat(itemList)
+                .isNotEqualTo(
+                        ItemList.builder()
+                                .setSelectable((index) -> {
+                                })
+                                .setSelectedIndex(1)
+                                .addItem(row)
+                                .addItem(row)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_missingSelectedListener() {
+        Row row = Row.builder().setTitle("Title").build();
+        ItemList itemList =
+                ItemList.builder().setSelectable((index) -> {
+                }).addItem(row).addItem(row).build();
+        assertThat(itemList).isNotEqualTo(ItemList.builder().addItem(row).addItem(row).build());
+    }
+
+    @Test
+    public void notEquals_missingVisibilityChangedListener() {
+        Row row = Row.builder().setTitle("Title").build();
+        ItemList itemList =
+                ItemList.builder()
+                        .setOnItemsVisibilityChangeListener((start, end) -> {
+                        })
+                        .addItem(row)
+                        .addItem(row)
+                        .build();
+        assertThat(itemList).isNotEqualTo(ItemList.builder().addItem(row).addItem(row).build());
+    }
+
+    @Test
+    public void notEquals_differentRows() {
+        Row row = Row.builder().setTitle("Title").build();
+        ItemList itemList = ItemList.builder().addItem(row).addItem(row).build();
+        assertThat(itemList).isNotEqualTo(ItemList.builder().addItem(row).build());
+    }
+
+    @Test
+    public void notEquals_differentGridItems() {
+        GridItem gridItem = GridItem.builder().setImage(BACK).setTitle("Title").build();
+        ItemList itemList = ItemList.builder().addItem(gridItem).addItem(gridItem).build();
+        assertThat(itemList).isNotEqualTo(ItemList.builder().addItem(gridItem).build());
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/LatLngTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/LatLngTest.java
new file mode 100644
index 0000000..c170d65
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/LatLngTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link LatLng}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LatLngTest {
+    @Test
+    public void createInstance() {
+        LatLng location = LatLng.create(123.f, 456.f);
+        assertThat(location.getLatitude()).isWithin(0.001).of(123.f);
+        assertThat(location.getLongitude()).isWithin(0.001).of(456.f);
+    }
+
+    @Test
+    public void equals() {
+        LatLng latLng = LatLng.create(123.45, 987.65);
+
+        assertThat(LatLng.create(123.45, 987.65)).isEqualTo(latLng);
+    }
+
+    @Test
+    public void notEquals_differentLat() {
+        LatLng latLng = LatLng.create(123.45, 987.65);
+
+        assertThat(LatLng.create(123.449999999, 987.65)).isNotEqualTo(latLng);
+        assertThat(LatLng.create(123.450000001, 987.65)).isNotEqualTo(latLng);
+    }
+
+    @Test
+    public void notEquals_differentLng() {
+        LatLng latLng = LatLng.create(123.45, 987.65);
+
+        assertThat(LatLng.create(123.45, 987.64999999999)).isNotEqualTo(latLng);
+        assertThat(LatLng.create(123.45, 987.65000000001)).isNotEqualTo(latLng);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java
new file mode 100644
index 0000000..fd05fa8c
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java
@@ -0,0 +1,531 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.text.SpannableString;
+
+import androidx.car.app.test.R;
+import androidx.car.app.utils.Logger;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link ListTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ListTemplateTest {
+    private final Logger mLogger = message -> {
+    };
+
+    @Test
+    public void createInstance_emptyList_notLoading_Throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> ListTemplate.builder().setTitle("Title").build());
+
+        // Positive case
+        ListTemplate.builder().setTitle("Title").setLoading(true).build();
+    }
+
+    @Test
+    public void createInstance_isLoading_hasList_Throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .setLoading(true)
+                                .setSingleList(getList())
+                                .build());
+    }
+
+    @Test
+    public void addEmptyList_throws() {
+        ItemList emptyList = ItemList.builder().build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ListTemplate.builder().setTitle("Title").addList(emptyList,
+                        "header").build());
+    }
+
+    @Test
+    public void addList_emptyHeader_throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ListTemplate.builder().setTitle("Title").addList(getList(), "").build());
+    }
+
+    @Test
+    public void resetList_clearsSingleList() {
+        ListTemplate.Builder builder =
+                ListTemplate.builder().setTitle("Title").setSingleList(getList());
+        assertThrows(IllegalStateException.class, () -> builder.clearAllLists().build());
+    }
+
+    @Test
+    public void resetList_clearsMultipleLists() {
+        ListTemplate.Builder builder =
+                ListTemplate.builder()
+                        .setTitle("Title")
+                        .addList(getList(), "header1")
+                        .addList(getList(), "header2");
+        assertThrows(IllegalStateException.class, () -> builder.clearAllLists().build());
+    }
+
+    @Test
+    public void addList_withVisibilityListener_throws() {
+        ItemList list =
+                ItemList.builder()
+                        .addItem(Row.builder().setTitle("Title").build())
+                        .setOnItemsVisibilityChangeListener((start, end) -> {
+                        })
+                        .build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ListTemplate.builder().setTitle("Title").addList(list, "header").build());
+    }
+
+    @Test
+    public void addList_moreThanMaxTexts_throws() {
+        Row rowExceedsMaxTexts =
+                Row.builder().setTitle("Title").addText("text1").addText("text2").addText(
+                        "text3").build();
+        Row rowMeetingMaxTexts =
+                Row.builder().setTitle("Title").addText("text1").addText("text2").build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(
+                                        ItemList.builder().addItem(rowExceedsMaxTexts).build())
+                                .build());
+
+        // Positive case.
+        ListTemplate.builder()
+                .setTitle("Title")
+                .setSingleList(ItemList.builder().addItem(rowMeetingMaxTexts).build())
+                .build();
+    }
+
+    @Test
+    public void createInstance_noHeaderTitleOrAction_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> ListTemplate.builder().setSingleList(getList()).build());
+
+        // Positive cases/.
+        ListTemplate.builder().setTitle("Title").setSingleList(getList()).build();
+        ListTemplate.builder().setHeaderAction(Action.BACK).setSingleList(getList()).build();
+    }
+
+    @Test
+    public void createInstance_setSingleList() {
+        ItemList list = getList();
+        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+        assertThat(template.getSingleList()).isEqualTo(list);
+        assertThat(template.getSectionLists()).isEmpty();
+    }
+
+    @Test
+    public void createInstance_addList() {
+        ItemList list1 = getList();
+        ItemList list2 = getList();
+        ListTemplate template =
+                ListTemplate.builder()
+                        .setTitle("Title")
+                        .addList(list1, "header1")
+                        .addList(list2, "header2")
+                        .build();
+        assertThat(template.getSingleList()).isNull();
+        assertThat(template.getSectionLists()).hasSize(2);
+        assertThat(template.getSectionLists().get(0).getItemList()).isEqualTo(list1);
+        assertThat(template.getSectionLists().get(0).getHeader().getText()).isEqualTo("header1");
+        assertThat(template.getSectionLists().get(1).getItemList()).isEqualTo(list2);
+        assertThat(template.getSectionLists().get(1).getHeader().getText()).isEqualTo("header2");
+    }
+
+    @Test
+    public void setSingleList_clearLists() {
+        ItemList list1 = getList();
+        ItemList list2 = getList();
+        ItemList list3 = getList();
+        ListTemplate template =
+                ListTemplate.builder()
+                        .setTitle("Title")
+                        .addList(list1, "header1")
+                        .addList(list2, "header2")
+                        .setSingleList(list3)
+                        .build();
+        assertThat(template.getSingleList()).isEqualTo(list3);
+        assertThat(template.getSectionLists()).isEmpty();
+    }
+
+    @Test
+    public void addList_clearSingleList() {
+        ItemList list1 = getList();
+        ItemList list2 = getList();
+        ItemList list3 = getList();
+        ListTemplate template =
+                ListTemplate.builder()
+                        .setTitle("Title")
+                        .setSingleList(list1)
+                        .addList(list2, "header1")
+                        .addList(list3, "header2")
+                        .build();
+        assertThat(template.getSingleList()).isNull();
+        assertThat(template.getSectionLists()).hasSize(2);
+    }
+
+    @Test
+    public void createInstance_setHeaderAction_invalidActionThrows() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        ListTemplate.builder()
+                                .setHeaderAction(
+                                        Action.builder().setTitle("Action").setOnClickListener(
+                                                () -> {
+                                                }).build()));
+    }
+
+    @Test
+    public void createInstance_setHeaderAction() {
+        ListTemplate template =
+                ListTemplate.builder().setSingleList(getList()).setHeaderAction(
+                        Action.BACK).build();
+        assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+    }
+
+    @Test
+    public void createInstance_setActionStrip() {
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        ListTemplate template =
+                ListTemplate.builder()
+                        .setTitle("Title")
+                        .setSingleList(getList())
+                        .setActionStrip(actionStrip)
+                        .build();
+        assertThat(template.getActionStrip()).isEqualTo(actionStrip);
+    }
+
+    @Test
+    public void validate_fromLoadingState_isRefresh() {
+        Row.Builder row = Row.builder().setTitle("Row1");
+        ItemList list = ItemList.builder().addItem(row.build()).build();
+        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+
+        // Going from loading state to new content is allowed.
+        assertThat(
+                template.isRefresh(
+                        ListTemplate.builder().setTitle("Title").setLoading(true).build(),
+                        mLogger))
+                .isTrue();
+    }
+
+    @Test
+    public void validate_mutableProperties_isRefresh() {
+        Row.Builder row = Row.builder().setTitle("Row1");
+        ItemList list = ItemList.builder().addItem(row.build()).build();
+        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+
+        // Ensure a template is a refresh of itself.
+        assertThat(template.isRefresh(template, mLogger)).isTrue();
+
+        // Allowed mutable states.
+        SpannableString stringWithSpan = new SpannableString("Row1");
+        stringWithSpan.setSpan(DurationSpan.create(1), 0, /* end= */ 1, /* flags= */ 0);
+        IconCompat icon = IconCompat.createWithResource(
+                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+        assertThat(
+                template.isRefresh(
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(
+                                        ItemList.builder()
+                                                .addItem(
+                                                        row.setOnClickListener(() -> {
+                                                        })
+                                                                .setBrowsable(true)
+                                                                .setTitle(stringWithSpan)
+                                                                .setImage(CarIcon.of(icon))
+                                                                .build())
+                                                .build())
+                                .setHeaderAction(Action.BACK)
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
+                                .build(),
+                        mLogger))
+                .isTrue();
+    }
+
+    @Test
+    public void validate_multipleListMutableProperties_isRefresh() {
+        Row row = Row.builder().setTitle("Row1").build();
+        Row refreshRow =
+                Row.builder()
+                        .setTitle("Row1")
+                        .setOnClickListener(() -> {
+                        })
+                        .setBrowsable(true)
+                        .setImage(
+                                CarIcon.of(
+                                        IconCompat.createWithResource(
+                                                ApplicationProvider.getApplicationContext(),
+                                                R.drawable.ic_test_1)))
+                        .build();
+        ItemList list = ItemList.builder().addItem(row).build();
+        ListTemplate template =
+                ListTemplate.builder()
+                        .setTitle("Title")
+                        .addList(list, "header1")
+                        .addList(list, "header2")
+                        .build();
+
+        // Sublist refreshes are allowed as long as headers and  number of sections remain the same.
+        assertThat(
+                template.isRefresh(
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .addList(ItemList.builder().addItem(refreshRow).build(), "header1")
+                                .addList(ItemList.builder().addItem(refreshRow).build(), "header2")
+                                .build(),
+                        mLogger))
+                .isTrue();
+    }
+
+    @Test
+    public void validate_titleUpdate_isNotRefresh() {
+        ItemList list = ItemList.builder().build();
+        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+
+        // Title updates are disallowed.
+        assertThat(
+                template.isRefresh(
+                        ListTemplate.builder().setSingleList(list).setTitle("Title2").build(),
+                        mLogger))
+                .isFalse();
+    }
+
+    @Test
+    public void validate_rowTextUpdate_isNotRefresh() {
+        Row.Builder row = Row.builder().setTitle("Row1");
+        ItemList list = ItemList.builder().addItem(row.build()).build();
+        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+
+        // Text updates are disallowed.
+        assertThat(
+                template.isRefresh(
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(ItemList.builder().addItem(
+                                        row.setTitle("Row2").build()).build())
+                                .build(),
+                        mLogger))
+                .isFalse();
+        assertThat(
+                template.isRefresh(
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(ItemList.builder().addItem(
+                                        row.addText("Text").build()).build())
+                                .build(),
+                        mLogger))
+                .isFalse();
+    }
+
+    @Test
+    public void validate_newRow_isNotRefresh() {
+        Row.Builder row = Row.builder().setTitle("Row1");
+        ItemList list = ItemList.builder().addItem(row.build()).build();
+        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+
+        // Additional rows are disallowed.
+        assertThat(
+                template.isRefresh(
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(
+                                        ItemList.builder().addItem(row.build()).addItem(
+                                                row.build()).build())
+                                .build(),
+                        mLogger))
+                .isFalse();
+    }
+
+    @Test
+    public void validate_multipleList_headerChange_isNotRefresh() {
+        Row.Builder row = Row.builder().setTitle("Row1");
+        ItemList list = ItemList.builder().addItem(row.build()).build();
+        ListTemplate template =
+                ListTemplate.builder().setTitle("Title").addList(list, "header1").build();
+
+        // Addition of lists are disallowed.
+        assertThat(
+                template.isRefresh(
+                        ListTemplate.builder().setTitle("Title").addList(list, "header2").build(),
+                        mLogger))
+                .isFalse();
+    }
+
+    @Test
+    public void validate_newList_isNotRefresh() {
+        Row.Builder row = Row.builder().setTitle("Row1");
+        ItemList list = ItemList.builder().addItem(row.build()).build();
+        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+
+        // Addition of lists are disallowed.
+        assertThat(
+                template.isRefresh(
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .addList(list, "header1")
+                                .addList(list, "header2")
+                                .build(),
+                        mLogger))
+                .isFalse();
+    }
+
+    @Test
+    public void validate_toLoadingState_isNotRefresh() {
+        // Going from content to loading state is disallowed.
+        assertThat(
+                ListTemplate.builder()
+                        .setTitle("Title")
+                        .setLoading(true)
+                        .build()
+                        .isRefresh(
+                                ListTemplate.builder()
+                                        .setTitle("Title")
+                                        .setSingleList(ItemList.builder().build())
+                                        .build(),
+                                mLogger))
+                .isFalse();
+    }
+
+    @Test
+    public void equals() {
+        ItemList itemList = ItemList.builder().build();
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        String title = "title";
+
+        ListTemplate template =
+                ListTemplate.builder()
+                        .setSingleList(itemList)
+                        .setActionStrip(actionStrip)
+                        .setHeaderAction(Action.BACK)
+                        .setTitle(title)
+                        .build();
+
+        assertThat(template)
+                .isEqualTo(
+                        ListTemplate.builder()
+                                .setSingleList(itemList)
+                                .setActionStrip(actionStrip)
+                                .setHeaderAction(Action.BACK)
+                                .setTitle(title)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentItemList() {
+        ItemList itemList = ItemList.builder().build();
+
+        ListTemplate template =
+                ListTemplate.builder().setTitle("Title").setSingleList(itemList).build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(
+                                        ItemList.builder().addItem(
+                                                Row.builder().setTitle("Title").build()).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentHeaderAction() {
+        ItemList itemList = ItemList.builder().build();
+
+        ListTemplate template =
+                ListTemplate.builder().setSingleList(itemList).setHeaderAction(Action.BACK).build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        ListTemplate.builder()
+                                .setSingleList(itemList)
+                                .setHeaderAction(Action.APP_ICON)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentActionStrip() {
+        ItemList itemList = ItemList.builder().build();
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+
+        ListTemplate template =
+                ListTemplate.builder()
+                        .setTitle("Title")
+                        .setSingleList(itemList)
+                        .setActionStrip(actionStrip)
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(itemList)
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentTitle() {
+        ItemList itemList = ItemList.builder().build();
+        String title = "title";
+
+        ListTemplate template = ListTemplate.builder().setSingleList(itemList).setTitle(
+                title).build();
+
+        assertThat(template)
+                .isNotEqualTo(ListTemplate.builder().setSingleList(itemList).setTitle(
+                        "yo").build());
+    }
+
+    private static ItemList getList() {
+        Row row1 = Row.builder().setTitle("Bananas").build();
+        Row row2 = Row.builder().setTitle("Oranges").build();
+        return ItemList.builder().addItem(row1).addItem(row2).build();
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/MessageTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/MessageTemplateTest.java
new file mode 100644
index 0000000..4784044
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/MessageTemplateTest.java
@@ -0,0 +1,361 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.car.app.test.R;
+import androidx.car.app.utils.Logger;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link MessageTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MessageTemplateTest {
+
+    private final String mTitle = "header";
+    private final String mDebugMessage = "debugMessage";
+    private final Throwable mCause = new IllegalStateException("bad");
+    private final String mMessage = "foo";
+    private final Action mAction = Action.BACK;
+    private final CarIcon mIcon = CarIcon.ALERT;
+
+    @Test
+    public void emptyMessage_throws() {
+        assertThrows(
+                IllegalStateException.class, () -> MessageTemplate.builder("").setTitle(
+                        mTitle).build());
+    }
+
+    @Test
+    public void invalidCarIcon_throws() {
+        Uri.Builder builder = new Uri.Builder();
+        builder.scheme(ContentResolver.SCHEME_CONTENT);
+        builder.appendPath("foo/bar");
+        Uri iconUri = builder.build();
+        CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> MessageTemplate.builder("hello").setTitle(mTitle).setIcon(carIcon));
+    }
+
+    @Test
+    public void noHeaderTitleOrAction_throws() {
+        assertThrows(IllegalStateException.class, () -> MessageTemplate.builder(mMessage).build());
+
+        // Positive cases.
+        MessageTemplate.builder(mMessage).setTitle(mTitle).build();
+        MessageTemplate.builder(mMessage).setHeaderAction(mAction).build();
+    }
+
+    @Test
+    public void createDefault_valuesAreNull() {
+        MessageTemplate template = MessageTemplate.builder(mMessage).setTitle(mTitle).build();
+        assertThat(template.getMessage().toString()).isEqualTo(mMessage);
+        assertThat(template.getTitle().getText()).isEqualTo("header");
+        assertThat(template.getIcon()).isNull();
+        assertThat(template.getHeaderAction()).isNull();
+        assertThat(template.getActionList()).isNull();
+        assertThat(template.getDebugMessage()).isNull();
+    }
+
+    @Test
+    public void createInstance_setHeaderAction_invalidActionThrows() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        MessageTemplate.builder(mMessage)
+                                .setHeaderAction(
+                                        Action.builder().setTitle("Action").setOnClickListener(
+                                                () -> {
+                                                }).build())
+                                .build());
+    }
+
+    @Test
+    public void createWithContents_hasProperValuesSet() {
+        Throwable exception = new IllegalStateException();
+        CarIcon icon = BACK;
+        Action action = Action.builder().setOnClickListener(() -> {
+        }).setTitle("foo").build();
+
+        MessageTemplate template =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setHeaderAction(Action.BACK)
+                        .setDebugCause(exception)
+                        .setIcon(icon)
+                        .setActions(ImmutableList.of(action))
+                        .build();
+
+        assertThat(template.getMessage().toString()).isEqualTo(mMessage);
+        assertThat(template.getTitle().toString()).isEqualTo(mTitle);
+        assertThat(template.getDebugMessage().toString()).isEqualTo(
+                Log.getStackTraceString(exception));
+        assertThat(template.getIcon()).isEqualTo(icon);
+        assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+        assertThat(template.getActionList().getList()).containsExactly(action);
+    }
+
+    @Test
+    public void validate_isRefresh() {
+        Logger logger = message -> {
+        };
+        MessageTemplate template = MessageTemplate.builder(mMessage).setTitle(mTitle).build();
+
+        assertThat(template.isRefresh(template, logger)).isTrue();
+
+        // Allowed mutable fields: icon, action strip and actions.
+        Action action = Action.builder().setOnClickListener(() -> {
+        }).setTitle("foo").build();
+        assertThat(
+                template.isRefresh(
+                        MessageTemplate.builder(mMessage)
+                                .setTitle(mTitle)
+                                .setIcon(
+                                        CarIcon.of(
+                                                IconCompat.createWithResource(
+                                                        ApplicationProvider.getApplicationContext(),
+                                                        R.drawable.ic_test_1)))
+                                .setHeaderAction(Action.BACK)
+                                .setActions(ImmutableList.of(action))
+                                .build(),
+                        logger))
+                .isTrue();
+
+        // Text changes are disallowed.
+        assertThat(
+                template.isRefresh(MessageTemplate.builder("Message2").setTitle(mTitle).build(),
+                        logger))
+                .isFalse();
+        assertThat(
+                template.isRefresh(
+                        MessageTemplate.builder(mMessage).setTitle(mTitle).setDebugMessage(
+                                "Debug").build(),
+                        logger))
+                .isFalse();
+        assertThat(
+                template.isRefresh(
+                        MessageTemplate.builder(mMessage)
+                                .setTitle(mTitle)
+                                .setDebugCause(new IllegalArgumentException("Exception"))
+                                .build(),
+                        logger))
+                .isFalse();
+        assertThat(
+                template.isRefresh(
+                        MessageTemplate.builder(mMessage).setTitle("Header2").build(), logger))
+                .isFalse();
+    }
+
+    @Test
+    public void equals() {
+        MessageTemplate template1 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setHeaderAction(Action.BACK)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+        MessageTemplate template2 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setHeaderAction(Action.BACK)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+
+        assertThat(template1).isEqualTo(template2);
+    }
+
+    @Test
+    public void notEquals_differentDebugMessage() {
+        MessageTemplate template1 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+        MessageTemplate template2 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage("yo")
+                        .setDebugCause(mCause)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+
+        assertThat(template1).isNotEqualTo(template2);
+    }
+
+    @Test
+    public void notEquals_differentCause() {
+        MessageTemplate template1 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+        MessageTemplate template2 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(new IllegalStateException("something else bad"))
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+
+        assertThat(template1).isNotEqualTo(template2);
+    }
+
+    @Test
+    public void notEquals_differentMessage() {
+        MessageTemplate template1 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setMessage(mMessage)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+        MessageTemplate template2 =
+                MessageTemplate.builder("bar")
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+
+        assertThat(template1).isNotEqualTo(template2);
+    }
+
+    @Test
+    public void notEquals_differentHeaderAction() {
+        MessageTemplate template1 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setHeaderAction(Action.BACK)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+        MessageTemplate template2 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setHeaderAction(Action.APP_ICON)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+
+        assertThat(template1).isNotEqualTo(template2);
+    }
+
+    @Test
+    public void notEquals_differentActions() {
+        MessageTemplate template1 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+        MessageTemplate template2 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setActions(ImmutableList.of(mAction, mAction))
+                        .setIcon(mIcon)
+                        .build();
+
+        assertThat(template1).isNotEqualTo(template2);
+    }
+
+    @Test
+    public void notEquals_differentIcon() {
+        MessageTemplate template1 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+        MessageTemplate template2 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(CarIcon.ERROR)
+                        .build();
+
+        assertThat(template1).isNotEqualTo(template2);
+    }
+
+    @Test
+    public void notEquals_differentTitle() {
+        MessageTemplate template1 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle(mTitle)
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+        MessageTemplate template2 =
+                MessageTemplate.builder(mMessage)
+                        .setTitle("Header2")
+                        .setDebugMessage(mDebugMessage)
+                        .setDebugCause(mCause)
+                        .setActions(ImmutableList.of(mAction))
+                        .setIcon(mIcon)
+                        .build();
+
+        assertThat(template1).isNotEqualTo(template2);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/MetadataTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/MetadataTest.java
new file mode 100644
index 0000000..a67ff2d
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/MetadataTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for the {@link Metadata} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MetadataTest {
+    @Test
+    public void setAndGetPlace() {
+        Place place = Place.builder(
+                LatLng.create(/* latitude= */ 123, /* longitude= */ 456)).build();
+        Metadata metadata = Metadata.ofPlace(place);
+        assertThat(metadata.getPlace()).isEqualTo(place);
+
+        metadata = Metadata.builder().build();
+        assertThat(metadata.getPlace()).isNull();
+    }
+
+    @Test
+    public void equals() {
+        Place place = Place.builder(
+                LatLng.create(/* latitude= */ 123, /* longitude= */ 456)).build();
+        Metadata metadata = Metadata.builder().setPlace(place).build();
+
+        assertThat(Metadata.builder().setPlace(place).build()).isEqualTo(metadata);
+    }
+
+    @Test
+    public void notEquals_differentPlace() {
+        Place place = Place.builder(
+                LatLng.create(/* latitude= */ 123, /* longitude= */ 456)).build();
+        Metadata metadata = Metadata.builder().setPlace(place).build();
+
+        Place place2 = Place.builder(
+                LatLng.create(/* latitude= */ 456, /* longitude= */ 789)).build();
+
+        assertThat(Metadata.builder().setPlace(place2).build()).isNotEqualTo(metadata);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ModelUtilsTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ModelUtilsTest.java
new file mode 100644
index 0000000..4a82bff
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ModelUtilsTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.car.app.model;
+
+import static org.junit.Assert.assertThrows;
+
+import android.text.SpannableString;
+
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link PlaceListMapTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ModelUtilsTest {
+    @Test
+    public void validateAllNonBrowsableRowsHaveDistances() {
+        DistanceSpan span =
+                DistanceSpan.create(
+                        Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
+        SpannableString stringWithDistance = new SpannableString("Test");
+        stringWithDistance.setSpan(span, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+        SpannableString stringWithInvalidDistance = new SpannableString("Test");
+        // 0-length span is not allowed.
+        stringWithInvalidDistance.setSpan(span, /* start= */ 0, /* end= */ 0, /* flags= */ 0);
+
+        Row rowWithDistance = Row.builder().setTitle(stringWithDistance).build();
+        Row rowWithDistance2 = Row.builder().setTitle("Title").addText(stringWithDistance).build();
+        Row rowWithInvalidDistance = Row.builder().setTitle(stringWithInvalidDistance).build();
+        Row rowWithoutDistance = Row.builder().setTitle("Test").build();
+        Row browsableRowWithoutPlace =
+                Row.builder().setTitle("Test").setBrowsable(true).setOnClickListener(() -> {
+                }).build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+                                ImmutableList.of(rowWithDistance, rowWithInvalidDistance)));
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+                                ImmutableList.of(rowWithDistance, rowWithoutDistance)));
+
+        // Positive cases
+        ModelUtils.validateAllNonBrowsableRowsHaveDistance(ImmutableList.of());
+        ModelUtils.validateAllNonBrowsableRowsHaveDistance(ImmutableList.of(rowWithDistance));
+        ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+                ImmutableList.of(rowWithDistance, rowWithDistance2));
+        ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+                ImmutableList.of(rowWithDistance, browsableRowWithoutPlace));
+        ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+                ImmutableList.of(browsableRowWithoutPlace));
+    }
+
+    @Test
+    public void validateAllRowsHaveDurationsOrDistances() {
+        DistanceSpan distanceSpan =
+                DistanceSpan.create(
+                        Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
+        DurationSpan durationSpan = DurationSpan.create(1);
+
+        SpannableString stringWithDistance = new SpannableString("Test");
+        stringWithDistance.setSpan(distanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+
+        SpannableString stringWithDuration = new SpannableString("Test");
+        stringWithDuration.setSpan(durationSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+
+        SpannableString stringWithInvalidDuration = new SpannableString("Test");
+        // 0-length span is not allowed.
+        stringWithInvalidDuration.setSpan(durationSpan, /* start= */ 0, /* end= */ 0, /* flags= */
+                0);
+
+        Row rowWithDistance = Row.builder().setTitle(stringWithDistance).build();
+        Row rowWithDuration = Row.builder().setTitle(stringWithDuration).build();
+        Row rowWithDuration2 = Row.builder().setTitle("Title").addText(stringWithDuration).build();
+        Row rowWithInvalidDuration = Row.builder().setTitle(stringWithInvalidDuration).build();
+        Row plainRow = Row.builder().setTitle("Test").build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        ModelUtils.validateAllRowsHaveDistanceOrDuration(
+                                ImmutableList.of(rowWithDuration, rowWithInvalidDuration)));
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        ModelUtils.validateAllRowsHaveDistanceOrDuration(
+                                ImmutableList.of(rowWithDuration, plainRow)));
+
+        // Positive cases.
+        ModelUtils.validateAllRowsHaveDistanceOrDuration(ImmutableList.of());
+        ModelUtils.validateAllRowsHaveDistanceOrDuration(ImmutableList.of(rowWithDistance));
+        ModelUtils.validateAllRowsHaveDistanceOrDuration(ImmutableList.of(rowWithDuration));
+        ModelUtils.validateAllRowsHaveDistanceOrDuration(
+                ImmutableList.of(rowWithDuration, rowWithDuration2));
+        ModelUtils.validateAllRowsHaveDistanceOrDuration(
+                ImmutableList.of(rowWithDuration, rowWithDistance));
+    }
+
+    @Test
+    public void validateAllRowsHaveOnlySmallSizedImages() {
+        CarIcon carIcon =
+                CarIcon.of(
+                        IconCompat.createWithResource(
+                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+        Row rowWithNoImage = Row.builder().setTitle("title1").build();
+        Row rowWithSmallImage =
+                Row.builder().setTitle("title2").setImage(carIcon, Row.IMAGE_TYPE_SMALL).build();
+        Row rowWithLargeImage =
+                Row.builder().setTitle("title3").setImage(carIcon, Row.IMAGE_TYPE_LARGE).build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ModelUtils.validateAllRowsHaveOnlySmallImages(
+                        ImmutableList.of(rowWithLargeImage)));
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        ModelUtils.validateAllRowsHaveOnlySmallImages(
+                                ImmutableList.of(rowWithNoImage, rowWithLargeImage)));
+
+        // Positive cases
+        ModelUtils.validateAllRowsHaveOnlySmallImages(ImmutableList.of());
+        ModelUtils.validateAllRowsHaveOnlySmallImages(ImmutableList.of(rowWithNoImage));
+        ModelUtils.validateAllRowsHaveOnlySmallImages(ImmutableList.of(rowWithSmallImage));
+        ModelUtils.validateAllRowsHaveOnlySmallImages(
+                ImmutableList.of(rowWithNoImage, rowWithSmallImage));
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/OnClickListenerWrapperTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/OnClickListenerWrapperTest.java
new file mode 100644
index 0000000..eb80ae2
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/OnClickListenerWrapperTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.car.app.model;
+
+/** Tests for {@link OnClickListenerWrapper}. */
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.RemoteException;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class OnClickListenerWrapperTest {
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    OnClickListener mMockOnClickListener;
+
+    @Test
+    public void create() throws RemoteException {
+        OnClickListenerWrapper wrapper = OnClickListenerWrapper.create(mMockOnClickListener);
+        assertThat(wrapper.isParkedOnly()).isFalse();
+
+        // TODO(shiufai): revisit the following as the test is not running on the main looper
+        //  thread, and thus the verify is failing.
+//        wrapper.getListener().onClick(mock(IOnDoneCallback.class));
+//        verify(mockOnClickListener).onClick();
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTemplateTest.java
new file mode 100644
index 0000000..6275cd6
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTemplateTest.java
@@ -0,0 +1,320 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.text.SpannableString;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.test.R;
+import androidx.car.app.utils.Logger;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link PaneTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PaneTemplateTest {
+
+    @Test
+    public void pane_moreThanMaxActions_throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> PaneTemplate.builder(TestUtils.createPane(2, 3)).setTitle("Title").build());
+
+        // Positive cases.
+        PaneTemplate.builder(TestUtils.createPane(2, 2)).setTitle("Title").build();
+    }
+
+    @Test
+    public void pane_moreThanMaxTexts_throws() {
+        Row rowExceedsMaxTexts =
+                Row.builder().setTitle("Title").addText("text1").addText("text2").addText(
+                        "text3").build();
+        Row rowMeetingMaxTexts =
+                Row.builder().setTitle("Title").addText("text1").addText("text2").build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PaneTemplate.builder(Pane.builder().addRow(rowExceedsMaxTexts).build())
+                                .setTitle("Title")
+                                .build());
+
+        // Positive cases.
+        PaneTemplate.builder(Pane.builder().addRow(rowMeetingMaxTexts).build())
+                .setTitle("Title")
+                .build();
+    }
+
+    @Test
+    public void pane_toggleOrClickListener_throws() {
+        Row rowWithToggle =
+                Row.builder().setTitle("Title").setToggle(Toggle.builder(isChecked -> {
+                }).build()).build();
+        Row rowWithClickListener = Row.builder().setTitle("Title").setOnClickListener(() -> {
+        }).build();
+        Row rowMeetingRestrictions =
+                Row.builder().setTitle("Title").addText("text1").addText("text2").build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PaneTemplate.builder(Pane.builder().addRow(rowWithToggle).build())
+                                .setTitle("Title")
+                                .build());
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PaneTemplate.builder(Pane.builder().addRow(rowWithClickListener).build())
+                                .setTitle("Title")
+                                .build());
+
+        // Positive cases.
+        PaneTemplate.builder(Pane.builder().addRow(rowMeetingRestrictions).build())
+                .setTitle("Title")
+                .build();
+    }
+
+    @Test
+    public void createInstance_noHeaderTitleOrAction_throws() {
+        assertThrows(IllegalStateException.class, () -> PaneTemplate.builder(getPane()).build());
+
+        // Positive cases.
+        PaneTemplate.builder(getPane()).setTitle("Title").build();
+        PaneTemplate.builder(getPane()).setHeaderAction(Action.BACK).build();
+    }
+
+    @Test
+    public void createInstance_setPane() {
+        Pane pane = getPane();
+        PaneTemplate template = PaneTemplate.builder(pane).setTitle("Title").build();
+        assertThat(template.getPane()).isEqualTo(pane);
+    }
+
+    @Test
+    public void createInstance_setHeaderAction_invalidActionThrows() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PaneTemplate.builder(getPane())
+                                .setHeaderAction(
+                                        Action.builder().setTitle("Action").setOnClickListener(
+                                                () -> {
+                                                }).build()));
+    }
+
+    @Test
+    public void createInstance_setHeaderAction() {
+        PaneTemplate template = PaneTemplate.builder(getPane()).setHeaderAction(
+                Action.BACK).build();
+        assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+    }
+
+    @Test
+    public void createInstance_setActionStrip() {
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        PaneTemplate template =
+                PaneTemplate.builder(getPane()).setTitle("Title").setActionStrip(
+                        actionStrip).build();
+        assertThat(template.getActionStrip()).isEqualTo(actionStrip);
+    }
+
+    @Test
+    public void validate_isRefresh() {
+        Logger logger = message -> {
+        };
+        Row.Builder row = Row.builder().setTitle("Row1");
+        PaneTemplate template =
+                PaneTemplate.builder(Pane.builder().addRow(row.build()).build()).setTitle(
+                        "Title").build();
+
+        assertThat(template.isRefresh(template, logger)).isTrue();
+
+        // Going from loading state to new content is allowed.
+        assertThat(
+                template.isRefresh(
+                        PaneTemplate.builder(Pane.builder().setLoading(true).build())
+                                .setTitle("Title")
+                                .build(),
+                        logger))
+                .isTrue();
+
+        // Other allowed mutable states.
+        SpannableString stringWithSpan = new SpannableString("Row1");
+        stringWithSpan.setSpan(DurationSpan.create(1), 0, /* end= */ 1, /* flags= */ 0);
+        IconCompat icon = IconCompat.createWithResource(
+                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+        assertThat(
+                template.isRefresh(
+                        PaneTemplate.builder(
+                                Pane.builder()
+                                        .addRow(
+                                                row.setImage(CarIcon.of(icon))
+                                                        .setTitle(stringWithSpan)
+                                                        .build())
+                                        .build())
+                                .setTitle("Title")
+                                .setHeaderAction(Action.BACK)
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
+                                .build(),
+                        logger))
+                .isTrue();
+
+        // Title updates are disallowed.
+        assertThat(
+                template.isRefresh(
+                        PaneTemplate.builder(Pane.builder().addRow(row.build()).build())
+                                .setTitle("Title2")
+                                .build(),
+                        logger))
+                .isFalse();
+
+        // Text updates are disallowed.
+        assertThat(
+                template.isRefresh(
+                        PaneTemplate.builder(
+                                Pane.builder().addRow(row.setTitle("Row2").build()).build())
+                                .setTitle("Title")
+                                .build(),
+                        logger))
+                .isFalse();
+        assertThat(
+                template.isRefresh(
+                        PaneTemplate.builder(
+                                Pane.builder().addRow(row.addText("Text").build()).build())
+                                .setTitle("Title")
+                                .build(),
+                        logger))
+                .isFalse();
+
+        // Additional rows are disallowed.
+        assertThat(
+                template.isRefresh(
+                        PaneTemplate.builder(Pane.builder().addRow(row.build()).addRow(
+                                row.build()).build())
+                                .setTitle("Title")
+                                .build(),
+                        logger))
+                .isFalse();
+
+        // Going from content to loading state is disallowed.
+        assertThat(
+                PaneTemplate.builder(Pane.builder().setLoading(true).build())
+                        .setTitle("Title")
+                        .build()
+                        .isRefresh(template, logger))
+                .isFalse();
+    }
+
+    @Test
+    public void equals() {
+        Pane pane = Pane.builder().addRow(Row.builder().setTitle("Title").build()).build();
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        String title = "foo";
+
+        PaneTemplate template =
+                PaneTemplate.builder(pane)
+                        .setHeaderAction(Action.BACK)
+                        .setActionStrip(actionStrip)
+                        .setTitle(title)
+                        .build();
+
+        assertThat(template)
+                .isEqualTo(
+                        PaneTemplate.builder(pane)
+                                .setHeaderAction(Action.BACK)
+                                .setActionStrip(actionStrip)
+                                .setTitle(title)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentPane() {
+        Pane pane = Pane.builder().addRow(Row.builder().setTitle("Title").build()).build();
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        String title = "foo";
+
+        PaneTemplate template =
+                PaneTemplate.builder(pane).setActionStrip(actionStrip).setTitle(title).build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        PaneTemplate.builder(
+                                Pane.builder().addRow(
+                                        Row.builder().setTitle("Title2").build()).build())
+                                .setActionStrip(actionStrip)
+                                .setTitle(title)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentHeaderAction() {
+        Pane pane = Pane.builder().addRow(Row.builder().setTitle("Title").build()).build();
+
+        PaneTemplate template = PaneTemplate.builder(pane).setHeaderAction(Action.BACK).build();
+
+        assertThat(template)
+                .isNotEqualTo(PaneTemplate.builder(pane).setHeaderAction(Action.APP_ICON).build());
+    }
+
+    @Test
+    public void notEquals_differentActionStrip() {
+        Pane pane = Pane.builder().addRow(Row.builder().setTitle("Title").build()).build();
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        String title = "foo";
+
+        PaneTemplate template =
+                PaneTemplate.builder(pane).setActionStrip(actionStrip).setTitle(title).build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        PaneTemplate.builder(pane)
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
+                                .setTitle(title)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentTitle() {
+        Pane pane = Pane.builder().addRow(Row.builder().setTitle("Title").build()).build();
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        String title = "foo";
+
+        PaneTemplate template =
+                PaneTemplate.builder(pane).setActionStrip(actionStrip).setTitle(title).build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        PaneTemplate.builder(pane).setActionStrip(actionStrip).setTitle(
+                                "bar").build());
+    }
+
+    private static Pane getPane() {
+        Row row1 = Row.builder().setTitle("Bananas").build();
+        Row row2 = Row.builder().setTitle("Oranges").build();
+        return Pane.builder().addRow(row1).addRow(row2).build();
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTest.java
new file mode 100644
index 0000000..2531bd6
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.text.SpannableString;
+
+import androidx.car.app.utils.Logger;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+/** Tests for {@link Pane}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PaneTest {
+    @Test
+    public void createEmptyRows_throws() {
+        assertThrows(IllegalStateException.class, () -> Pane.builder().build());
+
+        // Positive case
+        Pane.builder().setLoading(true).build();
+    }
+
+    @Test
+    public void isLoading_withRows_throws() {
+        Row row = createRow(1);
+        assertThrows(
+                IllegalStateException.class, () -> Pane.builder().addRow(row).setLoading(
+                        true).build());
+
+        // Positive case
+        Pane.builder().addRow(row).build();
+    }
+
+    @Test
+    public void addRow() {
+        Row row = createRow(1);
+        Pane pane = Pane.builder().addRow(row).build();
+        assertThat(pane.getRows()).containsExactly(row);
+    }
+
+    @Test
+    public void clearRows() {
+        Row row = createRow(1);
+        Pane pane = Pane.builder().addRow(row).addRow(row).clearRows().addRow(row).build();
+        assertThat(pane.getRows()).hasSize(1);
+    }
+
+    @Test
+    public void addRow_multiple() {
+        Row row1 = createRow(1);
+        Row row2 = createRow(2);
+        Row row3 = createRow(3);
+        Pane pane = Pane.builder().addRow(row1).addRow(row2).addRow(row3).build();
+        assertThat(pane.getRows()).containsExactly(row1, row2, row3);
+    }
+
+    @Test
+    public void setActions() {
+        Action action1 = createAction(1);
+        Action action2 = createAction(2);
+        List<Action> actions = Arrays.asList(action1, action2);
+        Pane pane =
+                Pane.builder().addRow(Row.builder().setTitle("Title").build()).setActions(
+                        actions).build();
+        assertActions(pane.getActionList(), actions);
+    }
+
+    @Test
+    public void setActions_throwsIfNullAction() {
+        Action action1 = createAction(1);
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Pane.builder().setActions(Arrays.asList(action1, null)).build());
+    }
+
+    @Test
+    public void validate_isRefresh() {
+        Logger logger = message -> {
+        };
+        Row.Builder row = Row.builder().setTitle("Title1");
+
+        Pane.Builder builder = Pane.builder().setLoading(true);
+        Pane pane = builder.build();
+        assertThat(pane.isRefresh(builder.build(), logger)).isTrue();
+
+        // Going from loading state to new content is allowed.
+        Pane paneWithRows = Pane.builder().addRow(row.build()).build();
+        assertThat(paneWithRows.isRefresh(pane, logger)).isTrue();
+
+        // Text updates are disallowed.
+        Pane paneWithDifferentTitle = Pane.builder().addRow(row.setTitle("Title2").build()).build();
+        Pane paneWithDifferentText = Pane.builder().addRow(row.addText("Text").build()).build();
+        assertThat(paneWithDifferentTitle.isRefresh(paneWithRows, logger)).isFalse();
+        assertThat(paneWithDifferentText.isRefresh(paneWithRows, logger)).isFalse();
+
+        // Additional rows are disallowed.
+        Pane paneWithTwoRows = Pane.builder().addRow(row.build()).addRow(row.build()).build();
+        assertThat(paneWithTwoRows.isRefresh(paneWithRows, logger)).isFalse();
+
+        // Going from content to loading state is disallowed.
+        assertThat(pane.isRefresh(paneWithRows, logger)).isFalse();
+    }
+
+    @Test
+    public void validate_isRefresh_differentSpansAreIgnored() {
+        Logger logger = message -> {
+        };
+        SpannableString textWithDistanceSpan = new SpannableString("Text");
+        textWithDistanceSpan.setSpan(
+                DistanceSpan.create(Distance.create(1000, Distance.UNIT_KILOMETERS)),
+                /* start= */ 0,
+                /* end= */ 1,
+                /* flags= */ 0);
+        SpannableString textWithDurationSpan = new SpannableString("Text");
+        textWithDurationSpan.setSpan(DurationSpan.create(1), 0, /* end= */ 1, /* flags= */ 0);
+
+        Pane pane1 =
+                Pane.builder()
+                        .addRow(
+                                Row.builder().setTitle(textWithDistanceSpan).addText(
+                                        textWithDurationSpan).build())
+                        .build();
+        Pane pane2 =
+                Pane.builder()
+                        .addRow(
+                                Row.builder().setTitle(textWithDurationSpan).addText(
+                                        textWithDistanceSpan).build())
+                        .build();
+        Pane pane3 =
+                Pane.builder().addRow(Row.builder().setTitle("Text2").addText(
+                        "Text2").build()).build();
+
+        assertThat(pane2.isRefresh(pane1, logger)).isTrue();
+        assertThat(pane3.isRefresh(pane1, logger)).isFalse();
+    }
+
+    @Test
+    public void equals() {
+        Pane pane =
+                Pane.builder()
+                        .setLoading(false)
+                        .setActions(ImmutableList.of(Action.APP_ICON, Action.BACK))
+                        .addRow(Row.builder().setTitle("Title").build())
+                        .build();
+
+        assertThat(pane)
+                .isEqualTo(
+                        Pane.builder()
+                                .setLoading(false)
+                                .setActions(ImmutableList.of(Action.APP_ICON, Action.BACK))
+                                .addRow(Row.builder().setTitle("Title").build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentLoading() {
+        Pane pane =
+                Pane.builder().setLoading(false).addRow(
+                        Row.builder().setTitle("Title").build()).build();
+
+        assertThat(pane).isNotEqualTo(Pane.builder().setLoading(true).build());
+    }
+
+    @Test
+    public void notEquals_differentActionsAdded() {
+        Row row = Row.builder().setTitle("Title").build();
+        Pane pane =
+                Pane.builder()
+                        .addRow(row)
+                        .setActions(ImmutableList.of(Action.APP_ICON, Action.BACK))
+                        .build();
+
+        assertThat(pane)
+                .isNotEqualTo(
+                        Pane.builder().addRow(row).setActions(
+                                ImmutableList.of(Action.APP_ICON)).build());
+    }
+
+    @Test
+    public void notEquals_differentRow() {
+        Pane pane = Pane.builder().addRow(Row.builder().setTitle("Title").build()).build();
+
+        assertThat(pane)
+                .isNotEqualTo(
+                        Pane.builder()
+                                .addRow(Row.builder().setTitle("Title").setOnClickListener(() -> {
+                                }).build())
+                                .build());
+    }
+
+    private static Row createRow(int suffix) {
+        return Row.builder().setTitle("The title " + suffix).addText(
+                "The subtitle " + suffix).build();
+    }
+
+    private static Action createAction(int suffix) {
+        return Action.builder().setTitle("Action " + suffix).setOnClickListener(() -> {
+        }).build();
+    }
+
+    private static void assertActions(Object obj, List<Action> expectedActions) {
+        ActionList actionList = (ActionList) obj;
+        assertThat(actionList.getList()).containsExactlyElementsIn(expectedActions);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
new file mode 100644
index 0000000..c4076f6
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.car.app.model;
+
+/** Tests for {@link OnClickListenerWrapper}. */
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.RemoteException;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ParkedOnlyOnClickListenerTest {
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    OnClickListener mMockOnClickListener;
+
+    @Test
+    public void create() throws RemoteException {
+        ParkedOnlyOnClickListener parkedOnlyOnClickListener =
+                ParkedOnlyOnClickListener.create(mMockOnClickListener);
+        OnClickListenerWrapper wrapper = OnClickListenerWrapper.create(parkedOnlyOnClickListener);
+
+        assertThat(wrapper.isParkedOnly()).isTrue();
+        // TODO(shiufai): revisit the following as the test is not running on the main looper
+        //  thread, and thus the verify is failing.
+//        wrapper.getListener().onClick(mock(IOnDoneCallback.class));
+//        verify(mockOnClickListener).onClick();
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceListMapTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceListMapTemplateTest.java
new file mode 100644
index 0000000..16257d0
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceListMapTemplateTest.java
@@ -0,0 +1,620 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.text.SpannableString;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.test.R;
+import androidx.car.app.utils.Logger;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link PlaceListMapTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PlaceListMapTemplateTest {
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final DistanceSpan mDistanceSpan =
+            DistanceSpan.create(
+                    Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
+
+    @Test
+    public void createInstance_emptyList_notLoading_Throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> PlaceListMapTemplate.builder().setTitle("Title").build());
+
+        // Positive case
+        PlaceListMapTemplate.builder().setTitle("Title").setLoading(true).build();
+    }
+
+    @Test
+    public void createInstance_isLoading_hasList_Throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setLoading(true)
+                                .setItemList(ItemList.builder().build())
+                                .build());
+    }
+
+    @Test
+    public void addList_selectable_throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(TestUtils.createItemListWithDistanceSpan(6, true,
+                                        mDistanceSpan))
+                                .build());
+
+        // Positive cases.
+        PlaceListMapTemplate.builder()
+                .setTitle("Title")
+                .setItemList(TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                .build();
+    }
+
+    @Test
+    public void addList_moreThanMaxTexts_throws() {
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+        Row rowExceedsMaxTexts =
+                Row.builder().setTitle(title).addText("text1").addText("text2").addText(
+                        "text3").build();
+        Row rowMeetingMaxTexts =
+                Row.builder().setTitle(title).addText("text1").addText("text2").build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(ItemList.builder().addItem(rowExceedsMaxTexts).build())
+                                .build());
+
+        // Positive cases.
+        PlaceListMapTemplate.builder()
+                .setTitle("Title")
+                .setItemList(ItemList.builder().addItem(rowMeetingMaxTexts).build())
+                .build();
+    }
+
+    @Test
+    public void addList_hasToggle_throws() {
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+        Row rowWithToggle =
+                Row.builder().setTitle(title).setToggle(Toggle.builder(isChecked -> {
+                }).build()).build();
+        Row rowMeetingRestrictions =
+                Row.builder().setTitle(title).addText("text1").addText("text2").build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(ItemList.builder().addItem(rowWithToggle).build())
+                                .build());
+
+        // Positive cases.
+        PlaceListMapTemplate.builder()
+                .setTitle("Title")
+                .setItemList(ItemList.builder().addItem(rowMeetingRestrictions).build())
+                .build();
+    }
+
+    @Test
+    public void createEmpty() {
+        ItemList itemList = ItemList.builder().build();
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder().setTitle("Title").setItemList(itemList).build();
+        assertThat(template.getItemList()).isEqualTo(itemList);
+        assertThat(template.getHeaderAction()).isNull();
+        assertThat(template.getActionStrip()).isNull();
+        assertThat(template.isCurrentLocationEnabled()).isFalse();
+    }
+
+    @Test
+    public void createInstance_setHeaderAction_invalidActionThrows() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListMapTemplate.builder()
+                                .setHeaderAction(
+                                        Action.builder().setTitle("Action").setOnClickListener(
+                                                () -> {
+                                                }).build()));
+    }
+
+    @Test
+    public void createInstance_setHeaderAction() {
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder()
+                        .setItemList(ItemList.builder().build())
+                        .setHeaderAction(Action.BACK)
+                        .build();
+
+        assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+    }
+
+    @Test
+    public void createInstance_notAllRowHaveDistances() {
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+        Row rowWithDistance = Row.builder().setTitle(title).build();
+        Row rowWithoutDistance = Row.builder().setTitle("Google Kir").build();
+        Row browsableRowWithoutDistance =
+                Row.builder()
+                        .setTitle("Google Kir")
+                        .setBrowsable(true)
+                        .setOnClickListener(() -> {
+                        })
+                        .build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        ItemList.builder()
+                                                .addItem(rowWithDistance)
+                                                .addItem(rowWithoutDistance)
+                                                .build()));
+
+        // Positive cases
+        PlaceListMapTemplate.builder()
+                .setTitle("Title")
+                .setItemList(ItemList.builder().addItem(rowWithDistance).build())
+                .build();
+        PlaceListMapTemplate.builder()
+                .setTitle("Title")
+                .setItemList(
+                        ItemList.builder()
+                                .addItem(rowWithDistance)
+                                .addItem(browsableRowWithoutDistance)
+                                .build())
+                .build();
+        PlaceListMapTemplate.builder()
+                .setTitle("Title")
+                .setItemList(ItemList.builder().addItem(browsableRowWithoutDistance).build())
+                .build();
+    }
+
+    @Test
+    public void createInstance_rowHasBothMarkerAndImages() {
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+        Row row =
+                Row.builder()
+                        .setTitle("Google Kir")
+                        .setOnClickListener(() -> {
+                        })
+                        .setImage(CarIcon.ALERT)
+                        .setMetadata(
+                                Metadata.ofPlace(
+                                        Place.builder(LatLng.create(10.f, 10.f))
+                                                .setMarker(PlaceMarker.getDefault())
+                                                .build()))
+                        .build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(ItemList.builder().addItem(row).build()));
+    }
+
+    @Test
+    public void createInstance_setItemList() {
+        ItemList itemList = TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan);
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder().setTitle("Title").setItemList(itemList).build();
+        assertThat(template.getItemList()).isEqualTo(itemList);
+    }
+
+    @Test
+    public void createInstance_setActionStrip() {
+        ItemList itemList = TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan);
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(itemList)
+                        .setActionStrip(actionStrip)
+                        .build();
+        assertThat(template.getActionStrip()).isEqualTo(actionStrip);
+    }
+
+    @Test
+    public void createInstance_setTitle() {
+        ItemList itemList = TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan);
+        String title = "title";
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder()
+                        .setItemList(itemList)
+                        .setTitle(title)
+                        .setCurrentLocationEnabled(true)
+                        .build();
+
+        assertThat(template.getTitle().getText()).isEqualTo(title);
+    }
+
+    @Test
+    public void createInstance_noHeaderTitleOrAction_throws() {
+        ItemList itemList = TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan);
+
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        PlaceListMapTemplate.builder()
+                                .setItemList(itemList)
+                                .setCurrentLocationEnabled(true)
+                                .build());
+
+        // Positive cases.
+        PlaceListMapTemplate.builder().setTitle("Title").setItemList(itemList).build();
+        PlaceListMapTemplate.builder().setHeaderAction(Action.BACK).setItemList(itemList).build();
+    }
+
+    @Test
+    public void validate_isRefresh() {
+        Logger logger = message -> {
+        };
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(mDistanceSpan, 0, 1, 0);
+        Row.Builder row =
+                Row.builder().setTitle(title).setBrowsable(true).setOnClickListener(() -> {
+                });
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(ItemList.builder().addItem(row.build()).build())
+                        .build();
+
+        assertThat(template.isRefresh(template, logger)).isTrue();
+
+        // Going from loading state to new content is allowed.
+        assertThat(
+                template.isRefresh(
+                        PlaceListMapTemplate.builder().setTitle("Title").setLoading(true).build(),
+                        logger))
+                .isTrue();
+
+        // Other allowed mutable states.
+        SpannableString stringWithSpan = new SpannableString("Title");
+        stringWithSpan.setSpan(mDistanceSpan, 1, /* end= */ 2, /* flags= */ 0);
+        ItemList itemList = ItemList.builder()
+                .addItem(
+                        row.setOnClickListener(() -> {
+                        })
+                                .setBrowsable(false)
+                                .setTitle(stringWithSpan)
+                                .setImage(
+                                        CarIcon.of(
+                                                IconCompat.createWithResource(
+                                                        ApplicationProvider.getApplicationContext(),
+                                                        R.drawable.ic_test_1)))
+                                .setMetadata(
+                                        Metadata.ofPlace(
+                                                Place.builder(
+                                                        LatLng.create(
+                                                                1,
+                                                                1)).build()))
+                                .build())
+                .build();
+        assertThat(
+                template.isRefresh(
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(itemList)
+                                .setHeaderAction(Action.BACK)
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
+                                .build(),
+                        logger))
+                .isTrue();
+
+        // Title updates are disallowed.
+        assertThat(
+                template.isRefresh(
+                        PlaceListMapTemplate.builder()
+                                .setItemList(ItemList.builder().addItem(row.build()).build())
+                                .setTitle("Title2")
+                                .build(),
+                        logger))
+                .isFalse();
+
+        // Text updates are disallowed.
+        SpannableString title2 = new SpannableString("Title2");
+        title2.setSpan(mDistanceSpan, 0, 1, 0);
+        assertThat(
+                template.isRefresh(
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(ItemList.builder().addItem(
+                                        row.setTitle(title2).build()).build())
+                                .build(),
+                        logger))
+                .isFalse();
+        assertThat(
+                template.isRefresh(
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        ItemList.builder()
+                                                .addItem(row.setTitle(title).addText(
+                                                        "Text").build())
+                                                .build())
+                                .build(),
+                        logger))
+                .isFalse();
+
+        // Additional rows are disallowed.
+        assertThat(
+                template.isRefresh(
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        ItemList.builder().addItem(row.build()).addItem(
+                                                row.build()).build())
+                                .build(),
+                        logger))
+                .isFalse();
+
+        // Going from content to loading state is disallowed.
+        assertThat(
+                PlaceListMapTemplate.builder()
+                        .setTitle("Title")
+                        .setLoading(true)
+                        .build()
+                        .isRefresh(template, logger))
+                .isFalse();
+    }
+
+    @Test
+    public void equals() {
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        String title = "foo";
+        Place place =
+                Place.builder(LatLng.create(123, 456))
+                        .setMarker(PlaceMarker.builder().setLabel("A").build())
+                        .build();
+
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder()
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .setHeaderAction(Action.BACK)
+                        .setActionStrip(actionStrip)
+                        .setTitle(title)
+                        .setAnchor(place)
+                        .setCurrentLocationEnabled(true)
+                        .build();
+
+        assertThat(template)
+                .isEqualTo(
+                        PlaceListMapTemplate.builder()
+                                .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+                                        mDistanceSpan))
+                                .setHeaderAction(Action.BACK)
+                                .setActionStrip(actionStrip)
+                                .setTitle(title)
+                                .setAnchor(place)
+                                .setCurrentLocationEnabled(true)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentList() {
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(ItemList.builder().build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentHeaderAction() {
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder()
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .setHeaderAction(Action.BACK)
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        PlaceListMapTemplate.builder()
+                                .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+                                        mDistanceSpan))
+                                .setHeaderAction(Action.APP_ICON)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentActionStrip() {
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .setActionStrip(actionStrip)
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+                                        mDistanceSpan))
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentTitle() {
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder()
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .setTitle("foo")
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        PlaceListMapTemplate.builder()
+                                .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+                                        mDistanceSpan))
+                                .setTitle("bar")
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentAnchor() {
+        Place place1 =
+                Place.builder(LatLng.create(123, 456))
+                        .setMarker(PlaceMarker.builder().setLabel("A").build())
+                        .build();
+
+        Place place2 =
+                Place.builder(LatLng.create(123, 456))
+                        .setMarker(PlaceMarker.builder().setLabel("B").build())
+                        .build();
+
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .setAnchor(place1)
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+                                        mDistanceSpan))
+                                .setAnchor(place2)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentLocationEnabled() {
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .setCurrentLocationEnabled(true)
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        PlaceListMapTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+                                        mDistanceSpan))
+                                .setCurrentLocationEnabled(false)
+                                .build());
+    }
+
+// TODO(shiufai): the following shadow is resulting in a ClasscastException.
+//  Further investigation is needed.
+//    @Test
+//    public void checkPermissions_hasPermissions() {
+//        PlaceListMapTemplate template =
+//                PlaceListMapTemplate.builder()
+//                        .setTitle("Title")
+//                        .setItemList(
+//                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+//                        .setCurrentLocationEnabled(true)
+//                        .build();
+//
+//        PackageManager packageManager = mContext.getPackageManager();
+//        PackageInfo pi = new PackageInfo();
+//        pi.packageName = mContext.getPackageName();
+//        pi.versionCode = 1;
+//        pi.requestedPermissions = new String[]{permission.ACCESS_FINE_LOCATION};
+//
+//        shadowOf(packageManager).installPackage(pi);
+//
+//        // Expect that it does not throw
+//        template.checkPermissions(context);
+//    }
+
+// TODO(shiufai): the following shadow is resulting in a ClasscastException.
+//  Further investigation is needed.
+//    @Test
+//    public void checkPermissions_doesNotHaveFineLocationPermission() {
+//        PlaceListMapTemplate template =
+//                PlaceListMapTemplate.builder()
+//                        .setTitle("Title")
+//                        .setItemList(
+//                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+//                        .setCurrentLocationEnabled(true)
+//                        .build();
+//
+//        PackageManager packageManager = mContext.getPackageManager();
+//        PackageInfo pi = new PackageInfo();
+//        pi.packageName = mContext.getPackageName();
+//        pi.versionCode = 1;
+//
+//        shadowOf(packageManager).installPackage(pi);
+//        assertThrows(SecurityException.class, () -> template.checkPermissions(context));
+//    }
+
+    @Test
+    public void checkPermissions_doesNotHavePermissions() {
+        PlaceListMapTemplate template =
+                PlaceListMapTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .setCurrentLocationEnabled(true)
+                        .build();
+
+        assertThrows(SecurityException.class, () -> template.checkPermissions(mContext));
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceMarkerTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceMarkerTest.java
new file mode 100644
index 0000000..3e7cc36
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceMarkerTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.car.app.model.CarIcon.ALERT;
+import static androidx.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link PlaceMarker}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PlaceMarkerTest {
+
+    @Test
+    public void create_throws_invalidLabelLength() {
+        assertThrows(IllegalArgumentException.class,
+                () -> PlaceMarker.builder().setLabel("Blah").build());
+    }
+
+    @Test
+    public void setColor_withImageTypeIcon_throws() {
+        CarIcon icon =
+                CarIcon.of(
+                        IconCompat.createWithResource(
+                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        PlaceMarker.builder()
+                                .setIcon(icon, PlaceMarker.TYPE_IMAGE)
+                                .setColor(CarColor.SECONDARY)
+                                .build());
+    }
+
+    @Test
+    public void create_throws_invalidCarIcon() {
+        Uri.Builder builder = new Uri.Builder();
+        builder.scheme(ContentResolver.SCHEME_CONTENT);
+        builder.appendPath("foo/bar");
+        Uri iconUri = builder.build();
+        CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> PlaceMarker.builder().setIcon(carIcon, PlaceMarker.TYPE_IMAGE));
+    }
+
+    @Test
+    public void createInstance() {
+        CarIcon icon =
+                CarIcon.of(
+                        IconCompat.createWithResource(
+                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+        PlaceMarker marker1 =
+                PlaceMarker.builder()
+                        .setIcon(icon, PlaceMarker.TYPE_ICON)
+                        .setLabel("foo")
+                        .setColor(CarColor.SECONDARY)
+                        .build();
+        assertThat(marker1.getIcon()).isEqualTo(icon);
+        assertThat(marker1.getIconType()).isEqualTo(PlaceMarker.TYPE_ICON);
+        assertThat(marker1.getColor()).isEqualTo(CarColor.SECONDARY);
+        assertThat(marker1.getLabel().getText()).isEqualTo("foo");
+    }
+
+    @Test
+    public void isDefaultMarker() {
+        assertThat(PlaceMarker.isDefaultMarker(null)).isFalse();
+        assertThat(PlaceMarker.isDefaultMarker(PlaceMarker.builder().setLabel("foo").build()))
+                .isFalse();
+
+        assertThat(PlaceMarker.isDefaultMarker(PlaceMarker.getDefault())).isTrue();
+        assertThat(PlaceMarker.isDefaultMarker(PlaceMarker.builder().build())).isTrue();
+    }
+
+    @Test
+    public void equals() {
+        CarIcon carIcon =
+                CarIcon.of(
+                        IconCompat.createWithResource(
+                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+        PlaceMarker marker =
+                PlaceMarker.builder()
+                        .setIcon(carIcon, PlaceMarker.TYPE_ICON)
+                        .setLabel("foo")
+                        .setColor(CarColor.SECONDARY)
+                        .build();
+
+        assertThat(
+                PlaceMarker.builder()
+                        .setIcon(carIcon, PlaceMarker.TYPE_ICON)
+                        .setLabel("foo")
+                        .setColor(CarColor.SECONDARY)
+                        .build())
+                .isEqualTo(marker);
+    }
+
+    @Test
+    public void notEquals_differentIcon() {
+        PlaceMarker marker = PlaceMarker.builder().setIcon(BACK, PlaceMarker.TYPE_IMAGE).build();
+
+        assertThat(PlaceMarker.builder().setIcon(ALERT, PlaceMarker.TYPE_IMAGE).build())
+                .isNotEqualTo(marker);
+    }
+
+    @Test
+    public void notEquals_differentLabel() {
+        PlaceMarker marker = PlaceMarker.builder().setLabel("foo").build();
+
+        assertThat(PlaceMarker.builder().setLabel("bar").build()).isNotEqualTo(marker);
+    }
+
+    @Test
+    public void notEquals_differentBackgroundColor() {
+        PlaceMarker marker = PlaceMarker.builder().setColor(CarColor.SECONDARY).build();
+
+        assertThat(PlaceMarker.builder().setColor(CarColor.BLUE).build()).isNotEqualTo(marker);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceTest.java
new file mode 100644
index 0000000..664dd26
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceTest.java
@@ -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.
+ */
+
+package androidx.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for the {@link Place} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PlaceTest {
+    /** Tests basic setter and getter operations. */
+    @Test
+    public void setAndGet() {
+        Place place =
+                Place.builder(LatLng.create(123, 456))
+                        .setMarker(PlaceMarker.builder().setLabel("A").build())
+                        .build();
+        assertThat(place.getLatLng()).isEqualTo(LatLng.create(123, 456));
+        assertThat(place.getMarker()).isEqualTo(PlaceMarker.builder().setLabel("A").build());
+    }
+
+    @Test
+    public void equals() {
+        Place place =
+                Place.builder(LatLng.create(123, 456))
+                        .setMarker(PlaceMarker.builder().setLabel("A").build())
+                        .build();
+
+        assertThat(place)
+                .isEqualTo(
+                        Place.builder(LatLng.create(123, 456))
+                                .setMarker(PlaceMarker.builder().setLabel("A").build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentLatLng() {
+        Place place = Place.builder(LatLng.create(123, 456)).build();
+
+        assertThat(place).isNotEqualTo(Place.builder(LatLng.create(1, 2)).build());
+    }
+
+    @Test
+    public void notEquals_differentMarker() {
+        Place place =
+                Place.builder(LatLng.create(123, 456))
+                        .setMarker(PlaceMarker.builder().setLabel("A").build())
+                        .build();
+
+        assertThat(place)
+                .isNotEqualTo(
+                        Place.builder(LatLng.create(123, 456))
+                                .setMarker(PlaceMarker.builder().setLabel("B").build())
+                                .build());
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/RowTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/RowTest.java
new file mode 100644
index 0000000..b818f03
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/RowTest.java
@@ -0,0 +1,275 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.car.app.model.CarIcon.ALERT;
+import static androidx.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Row}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RowTest {
+    @Test
+    public void create_defaultValues() {
+        Row row = Row.builder().setTitle("Title").build();
+        assertThat(row.getTitle().getText()).isEqualTo("Title");
+        assertThat(row.getTexts()).isEmpty();
+        assertThat(row.getImage()).isNull();
+        assertThat(row.getOnClickListener()).isNull();
+        assertThat(row.isBrowsable()).isFalse();
+        assertThat(row.getMetadata()).isEqualTo(Metadata.EMPTY_METADATA);
+        assertThat(row.getRowImageType()).isEqualTo(Row.IMAGE_TYPE_SMALL);
+    }
+
+    @Test
+    public void title_charSequence() {
+        String title = "foo";
+        Row row = Row.builder().setTitle(title).build();
+        assertThat(CarText.create(title)).isEqualTo(row.getTitle());
+    }
+
+    @Test
+    public void title_carText() {
+        CarText title = CarText.create("foo");
+        Row row = Row.builder().setTitle(title).build();
+        assertThat(title).isEqualTo(row.getTitle());
+    }
+
+    @Test
+    public void text_charSequence() {
+        CarText text1 = CarText.create("foo");
+        CarText text2 = CarText.create("bar");
+        Row row = Row.builder().setTitle("Title").addText(text1).addText(text2).build();
+        assertThat(row.getTexts()).containsExactly(text1, text2);
+    }
+
+    @Test
+    public void text_carText() {
+        String text1 = "foo";
+        String text2 = "bar";
+        Row row = Row.builder().setTitle("Title").addText(text1).addText(text2).build();
+        assertThat(row.getTexts()).containsExactly(CarText.create(text1), CarText.create(text2));
+    }
+
+    @Test
+    public void setImage() {
+        CarIcon image1 = BACK;
+        Row row = Row.builder().setTitle("Title").setImage(image1).build();
+        assertThat(image1).isEqualTo(row.getImage());
+    }
+
+    @Test
+    public void setToggle() {
+        Toggle toggle1 = Toggle.builder(isChecked -> {
+        }).build();
+        Row row = Row.builder().setTitle("Title").setToggle(toggle1).build();
+        assertThat(toggle1).isEqualTo(row.getToggle());
+    }
+
+    @Test
+    public void setSectionHeader() {
+        Row row =
+                Row.builder().setFlags(Row.ROW_FLAG_SECTION_HEADER).setTitle(
+                        "section header").build();
+        assertThat(row.getFlags() & Row.ROW_FLAG_SECTION_HEADER).isNotEqualTo(0);
+    }
+
+    @Test
+    public void setOnClickListenerAndToggle_throws() {
+        Toggle toggle1 = Toggle.builder(isChecked -> {
+        }).build();
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        Row.builder()
+                                .setTitle("Title")
+                                .setOnClickListener(() -> {
+                                })
+                                .setToggle(toggle1)
+                                .build());
+    }
+
+// TODO(shiufai): revisit the following as the test is not running on the main looper thread, and
+//  thus the verify is failing.
+//    @Test
+//    public void clickListener() throws RemoteException {
+//        OnClickListener >
+//        Row row = Row.builder().setTitle("Title").setOnClickListener(onClickListener).build();
+//        row.getOnClickListener().getListener().onClick(mock(IOnDoneCallback.class));
+//        verify(onClickListener).onClick();
+//    }
+
+    @Test
+    public void setMetadata() {
+        Metadata metadata = Metadata.ofPlace(Place.builder(LatLng.create(1, 1)).build());
+
+        Row row = Row.builder().setTitle("Title").setMetadata(metadata).build();
+        assertThat(row.getMetadata()).isEqualTo(metadata);
+    }
+
+    @Test
+    public void setIsBrowsable_noListener_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> Row.builder().setTitle("Title").setBrowsable(true).build());
+
+        // Positive case.
+        Row.builder().setTitle("Title").setBrowsable(false).build();
+    }
+
+    @Test
+    public void setIsBrowsable_notExclusivelyTextOrImage_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        Row.builder()
+                                .setTitle("Title")
+                                .setBrowsable(true)
+                                .setToggle(Toggle.builder(state -> {
+                                }).build())
+                                .build());
+
+        // Positive case.
+        Row.builder()
+                .setBrowsable(true)
+                .setOnClickListener(() -> {
+                })
+                .setTitle("Title")
+                .addText("Text")
+                .setImage(
+                        CarIcon.of(
+                                IconCompat.createWithResource(
+                                        ApplicationProvider.getApplicationContext(),
+                                        R.drawable.ic_test_1)))
+                .build();
+    }
+
+    @Test
+    public void equals() {
+        String title = "title";
+
+        Row row =
+                Row.builder()
+                        .setTitle(title)
+                        .setImage(BACK)
+                        .setOnClickListener(() -> {
+                        })
+                        .setBrowsable(false)
+                        .setFlags(1)
+                        .setMetadata(Metadata.EMPTY_METADATA)
+                        .addText(title)
+                        .build();
+
+        assertThat(
+                Row.builder()
+                        .setTitle(title)
+                        .setImage(BACK)
+                        .setOnClickListener(() -> {
+                        })
+                        .setBrowsable(false)
+                        .setFlags(1)
+                        .setMetadata(Metadata.EMPTY_METADATA)
+                        .addText(title)
+                        .build())
+                .isEqualTo(row);
+    }
+
+    @Test
+    public void notEquals_differentTitle() {
+        String title = "title";
+
+        Row row = Row.builder().setTitle(title).build();
+
+        assertThat(Row.builder().setTitle("foo").build()).isNotEqualTo(row);
+    }
+
+    @Test
+    public void notEquals_differentImage() {
+        Row row = Row.builder().setTitle("Title").setImage(BACK).build();
+
+        assertThat(Row.builder().setTitle("Title").setImage(ALERT).build()).isNotEqualTo(row);
+    }
+
+    @Test
+    public void notEquals_oneHasNoCallback() {
+        Row row = Row.builder().setTitle("Title").setOnClickListener(() -> {
+        }).build();
+
+        assertThat(Row.builder().setTitle("Title").build()).isNotEqualTo(row);
+    }
+
+    @Test
+    public void notEquals_differentBrowsable() {
+        Row row =
+                Row.builder().setTitle("Title").setBrowsable(false).setOnClickListener(() -> {
+                }).build();
+
+        assertThat(
+                Row.builder()
+                        .setTitle("Title")
+                        .setBrowsable(true)
+                        .setOnClickListener(() -> {
+                        })
+                        .build())
+                .isNotEqualTo(row);
+    }
+
+    @Test
+    public void notEquals_differentFlags() {
+        Row row = Row.builder().setTitle("Title").setFlags(1).build();
+
+        assertThat(Row.builder().setTitle("Title").setFlags(2).build()).isNotEqualTo(row);
+    }
+
+    @Test
+    public void notEquals_differentMetadata() {
+        Row row = Row.builder().setTitle("Title").setMetadata(Metadata.EMPTY_METADATA).build();
+
+        assertThat(
+                Row.builder()
+                        .setTitle("Title")
+                        .setMetadata(
+                                Metadata.builder()
+                                        .setPlace(
+                                                Place.builder(LatLng.create(/* latitude= */
+                                                        1f, /* longitude= */ 1f))
+                                                        .build())
+                                        .build())
+                        .build())
+                .isNotEqualTo(row);
+    }
+
+    @Test
+    public void notEquals_differenText() {
+        Row row = Row.builder().setTitle("Title").addText("foo").build();
+
+        assertThat(Row.builder().setTitle("Title").addText("bar").build()).isNotEqualTo(row);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/SearchTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/SearchTemplateTest.java
new file mode 100644
index 0000000..cdc70fb
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/SearchTemplateTest.java
@@ -0,0 +1,259 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.RemoteException;
+
+import androidx.car.app.SearchListener;
+import androidx.car.app.TestUtils;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/** Tests for {@link SearchTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SearchTemplateTest {
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    SearchListener mMockSearchListener;
+
+    @Test
+    public void createInstance_isLoading_hasList_Throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        SearchTemplate.builder(mMockSearchListener)
+                                .setLoading(true)
+                                .setItemList(ItemList.builder().build())
+                                .build());
+    }
+
+    @Test
+    public void addList_selectable_throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        SearchTemplate.builder(mMockSearchListener)
+                                .setItemList(TestUtils.createItemList(6, true))
+                                .build());
+
+        // Positive cases.
+        SearchTemplate.builder(mMockSearchListener)
+                .setItemList(TestUtils.createItemList(6, false))
+                .build();
+    }
+
+    @Test
+    public void addList_moreThanMaxTexts_throws() {
+        Row rowExceedsMaxTexts =
+                Row.builder().setTitle("Title").addText("text1").addText("text2").addText(
+                        "text3").build();
+        Row rowMeetingMaxTexts =
+                Row.builder().setTitle("Title").addText("text1").addText("text2").build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        SearchTemplate.builder(mMockSearchListener)
+                                .setItemList(ItemList.builder().addItem(rowExceedsMaxTexts).build())
+                                .build());
+
+        // Positive cases.
+        SearchTemplate.builder(mMockSearchListener)
+                .setItemList(ItemList.builder().addItem(rowMeetingMaxTexts).build())
+                .build();
+    }
+
+    @Test
+    public void addList_hasToggle_throws() {
+        Row rowWithToggle =
+                Row.builder().setTitle("Title").setToggle(Toggle.builder(isChecked -> {
+                }).build()).build();
+        Row rowMeetingRestrictions =
+                Row.builder().setTitle("Title").addText("text1").addText("text2").build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        SearchTemplate.builder(mMockSearchListener)
+                                .setItemList(ItemList.builder().addItem(rowWithToggle).build())
+                                .build());
+
+        // Positive cases.
+        SearchTemplate.builder(mMockSearchListener)
+                .setItemList(ItemList.builder().addItem(rowMeetingRestrictions).build())
+                .build();
+    }
+
+    @Test
+    public void buildEmpty_nullValues() {
+        SearchTemplate searchTemplate = SearchTemplate.builder(mMockSearchListener).build();
+
+        assertThat(searchTemplate.getInitialSearchText()).isNull();
+        assertThat(searchTemplate.getSearchHint()).isNull();
+        assertThat(searchTemplate.getActionStrip()).isNull();
+        assertThat(searchTemplate.getHeaderAction()).isNull();
+    }
+
+    @Test
+    public void buildWithValues() throws RemoteException {
+        String initialSearchText = "searchTemplate for this!!";
+        String searchHint = "This is not a hint";
+        ItemList itemList = ItemList.builder().addItem(
+                Row.builder().setTitle("foo").build()).build();
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+
+        SearchTemplate searchTemplate =
+                SearchTemplate.builder(mMockSearchListener)
+                        .setHeaderAction(Action.BACK)
+                        .setActionStrip(actionStrip)
+                        .setInitialSearchText(initialSearchText)
+                        .setSearchHint(searchHint)
+                        .setItemList(itemList)
+                        .build();
+
+        assertThat(searchTemplate.getInitialSearchText()).isEqualTo(initialSearchText);
+        assertThat(searchTemplate.getSearchHint()).isEqualTo(searchHint);
+        assertThat(searchTemplate.getItemList()).isEqualTo(itemList);
+        assertThat(searchTemplate.getActionStrip()).isEqualTo(actionStrip);
+        assertThat(searchTemplate.getHeaderAction()).isEqualTo(Action.BACK);
+
+        // TODO(shiufai): revisit the following as the test is not running on the main looper
+        //  thread, and thus the verify is failing.
+//        String searchText = "foo";
+//        searchTemplate.getSearchListener().onSearchSubmitted(searchText,
+//                mock(IOnDoneCallback.class));
+//        verify(mockSearchListener).onSearchSubmitted(searchText);
+    }
+
+    @Test
+    public void createInstance_setHeaderAction_invalidActionThrows() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        SearchTemplate.builder(mMockSearchListener)
+                                .setHeaderAction(
+                                        Action.builder().setTitle("Action").setOnClickListener(
+                                                () -> {
+                                                }).build()));
+    }
+
+    @Test
+    public void equals() {
+        SearchTemplate template =
+                SearchTemplate.builder(mMockSearchListener)
+                        .setHeaderAction(Action.BACK)
+                        .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+                        .setInitialSearchText("foo")
+                        .setSearchHint("hint")
+                        .setShowKeyboardByDefault(false)
+                        .setLoading(false)
+                        .setItemList(ItemList.builder().build())
+                        .build();
+
+        assertThat(template)
+                .isEqualTo(
+                        SearchTemplate.builder(mMockSearchListener)
+                                .setHeaderAction(Action.BACK)
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.BACK).build())
+                                .setInitialSearchText("foo")
+                                .setSearchHint("hint")
+                                .setShowKeyboardByDefault(false)
+                                .setLoading(false)
+                                .setItemList(ItemList.builder().build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentHeaderAction() {
+        SearchTemplate template =
+                SearchTemplate.builder(mMockSearchListener).setHeaderAction(Action.BACK).build();
+        assertThat(template)
+                .isNotEqualTo(
+                        SearchTemplate.builder(mMockSearchListener).setHeaderAction(
+                                Action.APP_ICON).build());
+    }
+
+    @Test
+    public void notEquals_differentActionStrip() {
+        SearchTemplate template =
+                SearchTemplate.builder(mMockSearchListener)
+                        .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+                        .build();
+        assertThat(template)
+                .isNotEqualTo(
+                        SearchTemplate.builder(mMockSearchListener)
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentInitialSearchText() {
+        SearchTemplate template =
+                SearchTemplate.builder(mMockSearchListener).setInitialSearchText("foo").build();
+        assertThat(template)
+                .isNotEqualTo(
+                        SearchTemplate.builder(mMockSearchListener).setInitialSearchText(
+                                "bar").build());
+    }
+
+    @Test
+    public void notEquals_differentSearchHint() {
+        SearchTemplate template =
+                SearchTemplate.builder(mMockSearchListener).setSearchHint("foo").build();
+        assertThat(template)
+                .isNotEqualTo(SearchTemplate.builder(mMockSearchListener).setSearchHint(
+                        "bar").build());
+    }
+
+    @Test
+    public void notEquals_differentKeyboardEnabled() {
+        SearchTemplate template =
+                SearchTemplate.builder(mMockSearchListener).setShowKeyboardByDefault(true).build();
+        assertThat(template)
+                .isNotEqualTo(
+                        SearchTemplate.builder(mMockSearchListener).setShowKeyboardByDefault(
+                                false).build());
+    }
+
+    @Test
+    public void notEquals_differentItemList() {
+        SearchTemplate template =
+                SearchTemplate.builder(mMockSearchListener).setItemList(
+                        ItemList.builder().build()).build();
+        assertThat(template)
+                .isNotEqualTo(
+                        SearchTemplate.builder(mMockSearchListener)
+                                .setItemList(
+                                        ItemList.builder().addItem(
+                                                Row.builder().setTitle("Title").build()).build())
+                                .build());
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/SectionedItemListTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/SectionedItemListTest.java
new file mode 100644
index 0000000..69f5124
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/SectionedItemListTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link ItemListTest}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SectionedItemListTest {
+
+    @Test
+    public void createInstance() {
+        ItemList list = ItemList.builder().build();
+        CarText header = CarText.create("header");
+        SectionedItemList sectionList = SectionedItemList.create(list, header);
+
+        assertThat(sectionList.getItemList()).isEqualTo(list);
+        assertThat(sectionList.getHeader()).isEqualTo(header);
+    }
+
+    @Test
+    public void equals() {
+        ItemList list = ItemList.builder().build();
+        CarText header = CarText.create("header");
+        SectionedItemList sectionList = SectionedItemList.create(list, header);
+
+        ItemList list2 = ItemList.builder().build();
+        CarText header2 = CarText.create("header");
+        SectionedItemList sectionList2 = SectionedItemList.create(list2, header2);
+
+        assertThat(sectionList2).isEqualTo(sectionList);
+    }
+
+    @Test
+    public void notEquals_differentItemList() {
+        ItemList list = ItemList.builder().addItem(Row.builder().setTitle("Title").build()).build();
+        CarText header = CarText.create("header");
+        SectionedItemList sectionList = SectionedItemList.create(list, header);
+
+        ItemList list2 = ItemList.builder().build();
+        CarText header2 = CarText.create("header");
+        SectionedItemList sectionList2 = SectionedItemList.create(list2, header2);
+
+        assertThat(sectionList2).isNotEqualTo(sectionList);
+    }
+
+    @Test
+    public void notEquals_differentHeader() {
+        ItemList list = ItemList.builder().build();
+        CarText header = CarText.create("header1");
+        SectionedItemList sectionList = SectionedItemList.create(list, header);
+
+        ItemList list2 = ItemList.builder().build();
+        CarText header2 = CarText.create("header2");
+        SectionedItemList sectionList2 = SectionedItemList.create(list2, header2);
+
+        assertThat(sectionList2).isNotEqualTo(sectionList);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/TemplateWrapperTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/TemplateWrapperTest.java
new file mode 100644
index 0000000..3cbb446
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/TemplateWrapperTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link TemplateWrapper}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TemplateWrapperTest {
+    @Test
+    public void createInstance() {
+        ListTemplate template =
+                ListTemplate.builder().setTitle("Title").setSingleList(
+                        ItemList.builder().build()).build();
+        TemplateWrapper wrapper = TemplateWrapper.wrap(template);
+        assertThat(wrapper.getTemplate()).isEqualTo(template);
+
+        wrapper = TemplateWrapper.wrap(template, "1");
+        assertThat(wrapper.getTemplate()).isEqualTo(template);
+        assertThat(wrapper.getId()).isEqualTo("1");
+    }
+
+    @Test
+    public void createInstance_thenUpdate() {
+        ListTemplate template =
+                ListTemplate.builder().setTitle("Title").setSingleList(
+                        ItemList.builder().build()).build();
+        ListTemplate template2 =
+                ListTemplate.builder().setTitle("Title").setSingleList(
+                        ItemList.builder().build()).build();
+
+        TemplateWrapper wrapper = TemplateWrapper.wrap(template);
+        String id = wrapper.getId();
+        assertThat(wrapper.getTemplate()).isEqualTo(template);
+        assertThat(wrapper.getCurrentTaskStep()).isEqualTo(0);
+
+        wrapper.setTemplate(template2);
+        assertThat(wrapper.getTemplate()).isEqualTo(template2);
+        assertThat(wrapper.getCurrentTaskStep()).isEqualTo(0);
+        assertThat(wrapper.getId()).isEqualTo(id);
+
+        wrapper.setCurrentTaskStep(2);
+        assertThat(wrapper.getTemplate()).isEqualTo(template2);
+        assertThat(wrapper.getCurrentTaskStep()).isEqualTo(2);
+        assertThat(wrapper.getId()).isEqualTo(id);
+
+        wrapper.setRefresh(true);
+        assertThat(wrapper.isRefresh()).isTrue();
+        assertThat(wrapper.getTemplate()).isEqualTo(template2);
+        assertThat(wrapper.getCurrentTaskStep()).isEqualTo(2);
+        assertThat(wrapper.getId()).isEqualTo(id);
+
+        wrapper.setId("1");
+        assertThat(wrapper.getId()).isEqualTo("1");
+    }
+
+    @Test
+    public void copyOf() {
+        ListTemplate template =
+                ListTemplate.builder().setTitle("Title").setSingleList(
+                        ItemList.builder().build()).build();
+        TemplateWrapper source = TemplateWrapper.wrap(template, "ID");
+        source.setCurrentTaskStep(45);
+        source.setRefresh(true);
+
+        TemplateWrapper dest = TemplateWrapper.copyOf(source);
+        assertThat(dest.getTemplate()).isEqualTo(template);
+        assertThat(dest.getCurrentTaskStep()).isEqualTo(45);
+        assertThat(dest.getId()).isEqualTo("ID");
+        assertThat(dest.isRefresh()).isTrue();
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ToggleTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ToggleTest.java
new file mode 100644
index 0000000..ab4d025
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ToggleTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.car.app.model.Toggle.OnCheckedChangeListener;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/** Tests for {@link Toggle}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ToggleTest {
+    @Rule
+    public MockitoRule mocks = MockitoJUnit.rule();
+
+    @Mock
+    OnCheckedChangeListener mMockOnCheckedChangeListener;
+
+    @Test
+    public void build_withValues_notCheckedByDefault() {
+        Toggle toggle = Toggle.builder(mMockOnCheckedChangeListener).build();
+        assertThat(toggle.isChecked()).isFalse();
+    }
+
+// TODO(shiufai): revisit the following as the test is not running on the main looper
+//  thread, and thus the verify is failing.
+//    @Test
+//    public void build_checkedChange_sendsCheckedChangeCall() throws RemoteException {
+//        Toggle toggle = Toggle.builder(mockOnCheckedChangeListener).setChecked(true).build();
+//
+//        toggle.getOnCheckedChangeListener().onCheckedChange(false, mock(IOnDoneCallback.class));
+//        verify(mockOnCheckedChangeListener).onCheckedChange(false);
+//    }
+
+    @Test
+    public void equals() {
+        Toggle toggle = Toggle.builder(mMockOnCheckedChangeListener).setChecked(true).build();
+        assertThat(toggle)
+                .isEqualTo(Toggle.builder(mMockOnCheckedChangeListener).setChecked(true).build());
+    }
+
+    @Test
+    public void notEquals() {
+        Toggle toggle = Toggle.builder(mMockOnCheckedChangeListener).setChecked(true).build();
+        assertThat(toggle)
+                .isNotEqualTo(Toggle.builder(mMockOnCheckedChangeListener).setChecked(
+                        false).build());
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
new file mode 100644
index 0000000..ab577b5
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.car.app.model.constraints;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+
+/** Tests for {@link ActionsConstraints}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActionsConstraintsTest {
+    @Test
+    public void createEmpty() {
+        ActionsConstraints constraints = ActionsConstraints.builder().build();
+
+        assertThat(constraints.getMaxActions()).isEqualTo(Integer.MAX_VALUE);
+        assertThat(constraints.getRequiredActionTypes()).isEmpty();
+    }
+
+    @Test
+    public void create_requiredExceedsMaxAllowedActions() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        ActionsConstraints.builder()
+                                .setMaxActions(1)
+                                .addRequiredActionType(Action.TYPE_BACK)
+                                .addRequiredActionType(Action.TYPE_CUSTOM)
+                                .build());
+    }
+
+    @Test
+    public void create_requiredAlsoDisallowed() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        ActionsConstraints.builder()
+                                .addRequiredActionType(Action.TYPE_BACK)
+                                .addDisallowedActionType(Action.TYPE_BACK)
+                                .build());
+    }
+
+    @Test
+    public void createConstraints() {
+        ActionsConstraints constraints =
+                ActionsConstraints.builder()
+                        .setMaxActions(2)
+                        .addRequiredActionType(Action.TYPE_CUSTOM)
+                        .addDisallowedActionType(Action.TYPE_BACK)
+                        .build();
+
+        assertThat(constraints.getMaxActions()).isEqualTo(2);
+        assertThat(constraints.getRequiredActionTypes()).containsExactly(Action.TYPE_CUSTOM);
+        assertThat(constraints.getDisallowedActionTypes()).containsExactly(Action.TYPE_BACK);
+    }
+
+    @Test
+    public void validateActions() {
+        ActionsConstraints constraints =
+                ActionsConstraints.builder()
+                        .setMaxActions(2)
+                        .setMaxCustomTitles(1)
+                        .addRequiredActionType(Action.TYPE_CUSTOM)
+                        .addDisallowedActionType(Action.TYPE_BACK)
+                        .build();
+
+        CarIcon carIcon =
+                CarIcon.of(
+                        IconCompat.createWithResource(
+                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+        Action actionWithIcon = TestUtils.createAction(null, carIcon);
+        Action actionWithTitle = TestUtils.createAction("Title", carIcon);
+
+        // Positive case: instance that fits the 2-max-actions, only-1-has-title constraint.
+        constraints.validateOrThrow(
+                ActionStrip.builder()
+                        .addAction(actionWithIcon)
+                        .addAction(actionWithTitle)
+                        .build()
+                        .getActions());
+        // Positive case: empty list is okay when there are no required types
+        ActionsConstraints.builder().setMaxActions(2).build().validateOrThrow(
+                Collections.emptyList());
+
+        // Missing required type.
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        constraints.validateOrThrow(
+                                ActionStrip.builder().addAction(
+                                        Action.APP_ICON).build().getActions()));
+
+        // Disallowed type
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        constraints.validateOrThrow(
+                                ActionStrip.builder().addAction(Action.BACK).build().getActions()));
+
+        // Over max allowed actions
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        constraints.validateOrThrow(
+                                ActionStrip.builder()
+                                        .addAction(Action.APP_ICON)
+                                        .addAction(actionWithIcon)
+                                        .addAction(actionWithTitle)
+                                        .build()
+                                        .getActions()));
+
+        // Over max allowed actions with title
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        constraints.validateOrThrow(
+                                ActionStrip.builder()
+                                        .addAction(actionWithTitle)
+                                        .addAction(actionWithTitle)
+                                        .build()
+                                        .getActions()));
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowConstraintsTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowConstraintsTest.java
new file mode 100644
index 0000000..9b4d96e
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowConstraintsTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.car.app.model.constraints;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Toggle;
+import androidx.car.app.test.R;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link RowConstraints}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RowConstraintsTest {
+    @Test
+    public void validate_clickListener() {
+        RowConstraints constraints = RowConstraints.builder().setOnClickListenerAllowed(
+                false).build();
+        RowConstraints allowConstraints =
+                RowConstraints.builder().setOnClickListenerAllowed(true).build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        constraints.validateOrThrow(
+                                Row.builder().setTitle("Title)").setOnClickListener(() -> {
+                                }).build()));
+
+        // Positive cases
+        constraints.validateOrThrow(Row.builder().setTitle("Title").build());
+        allowConstraints.validateOrThrow(
+                Row.builder().setTitle("Title").setOnClickListener(() -> {
+                }).build());
+    }
+
+    @Test
+    public void validate_toggle() {
+        RowConstraints constraints = RowConstraints.builder().setToggleAllowed(false).build();
+        RowConstraints allowConstraints = RowConstraints.builder().setToggleAllowed(true).build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        constraints.validateOrThrow(
+                                Row.builder()
+                                        .setTitle("Title)")
+                                        .setToggle(Toggle.builder(isChecked -> {
+                                        }).build())
+                                        .build()));
+
+        // Positive cases
+        constraints.validateOrThrow(Row.builder().setTitle("Title").build());
+        allowConstraints.validateOrThrow(
+                Row.builder().setTitle("Title").setToggle(Toggle.builder(isChecked -> {
+                }).build()).build());
+    }
+
+    @Test
+    public void validate_images() {
+        RowConstraints constraints = RowConstraints.builder().setImageAllowed(false).build();
+        RowConstraints allowConstraints = RowConstraints.builder().setImageAllowed(true).build();
+        CarIcon carIcon =
+                CarIcon.of(
+                        IconCompat.createWithResource(
+                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        constraints.validateOrThrow(
+                                Row.builder().setTitle("Title)").setImage(carIcon).build()));
+
+        // Positive cases
+        constraints.validateOrThrow(Row.builder().setTitle("Title").build());
+        allowConstraints.validateOrThrow(Row.builder().setTitle("Title").setImage(carIcon).build());
+    }
+
+    @Test
+    public void validate_texts() {
+        RowConstraints constraints = RowConstraints.builder().setMaxTextLinesPerRow(2).build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        constraints.validateOrThrow(
+                                Row.builder()
+                                        .setTitle("Title)")
+                                        .addText("text1")
+                                        .addText("text2")
+                                        .addText("text3")
+                                        .build()));
+
+        // Positive cases
+        constraints.validateOrThrow(
+                Row.builder().setTitle("Title").addText("text1").addText("text2").build());
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowListConstraintsTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
new file mode 100644
index 0000000..56b5273
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.car.app.model.constraints;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.TestUtils;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link RowListConstraints}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RowListConstraintsTest {
+    @Test
+    public void validate_itemList_noSelectable() {
+        RowListConstraints disallowConstraints =
+                RowListConstraints.builder()
+                        .setRowListType(RowListConstraints.DEFAULT_LIST)
+                        .setAllowSelectableLists(false)
+                        .build();
+        RowListConstraints allowConstraints =
+                RowListConstraints.builder()
+                        .setRowListType(RowListConstraints.DEFAULT_LIST)
+                        .setAllowSelectableLists(true)
+                        .build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> disallowConstraints.validateOrThrow(TestUtils.createItemList(5, true)));
+
+        // Positive case
+        disallowConstraints.validateOrThrow(TestUtils.createItemList(5, false));
+        allowConstraints.validateOrThrow(TestUtils.createItemList(5, true));
+    }
+
+    @Test
+    public void validate_sectionItemList_noSelectable() {
+        RowListConstraints disallowConstraints =
+                RowListConstraints.builder()
+                        .setRowListType(RowListConstraints.DEFAULT_LIST)
+                        .setAllowSelectableLists(false)
+                        .build();
+        RowListConstraints allowConstraints =
+                RowListConstraints.builder()
+                        .setRowListType(RowListConstraints.DEFAULT_LIST)
+                        .setAllowSelectableLists(true)
+                        .build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> disallowConstraints.validateOrThrow(TestUtils.createSections(2, 2, true)));
+
+        // Positive case
+        disallowConstraints.validateOrThrow(TestUtils.createSections(2, 2, false));
+        allowConstraints.validateOrThrow(TestUtils.createSections(2, 2, true));
+    }
+
+    @Test
+    public void validate_pane_maxActions() {
+        RowListConstraints constraints =
+                RowListConstraints.builder()
+                        .setRowListType(RowListConstraints.DEFAULT_LIST)
+                        .setMaxActions(2)
+                        .build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> constraints.validateOrThrow(TestUtils.createPane(5, 3)));
+
+        // Positive case
+        constraints.validateOrThrow(TestUtils.createPane(5, 2));
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/NavigationManagerTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/NavigationManagerTest.java
new file mode 100644
index 0000000..f9e4494
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/NavigationManagerTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.car.app.navigation;
+
+import static androidx.car.app.TestUtils.createDateTimeWithZone;
+
+import android.os.RemoteException;
+
+import androidx.car.app.HostDispatcher;
+import androidx.car.app.ICarHost;
+import androidx.car.app.model.Distance;
+import androidx.car.app.navigation.model.Destination;
+import androidx.car.app.navigation.model.Step;
+import androidx.car.app.navigation.model.TravelEstimate;
+import androidx.car.app.navigation.model.Trip;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.concurrent.TimeUnit;
+
+/** Tests for {@link NavigationManager}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NavigationManagerTest {
+    @Mock
+    private ICarHost mMockCarHost;
+    @Mock
+    private INavigationHost.Stub mMockNavHost;
+    @Mock
+    private NavigationManagerListener mNavigationListener;
+
+    private final HostDispatcher mHostDispatcher = new HostDispatcher();
+    private NavigationManager mNavigationManager;
+
+    private final Destination mDestination =
+            Destination.builder().setName("Home").setAddress("123 State Street").build();
+    private final Step mStep = Step.builder("Straight Ahead").build();
+    private final TravelEstimate mStepTravelEstimate =
+            TravelEstimate.create(
+                    Distance.create(/* displayDistance= */ 10, Distance.UNIT_KILOMETERS),
+                    TimeUnit.HOURS.toSeconds(1),
+                    createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific"));
+    private final TravelEstimate mDestinationTravelEstimate =
+            TravelEstimate.create(
+                    Distance.create(/* displayDistance= */ 100, Distance.UNIT_KILOMETERS),
+                    TimeUnit.HOURS.toSeconds(1),
+                    createDateTimeWithZone("2020-04-14T16:57:00", "US/Pacific"));
+    private static final String CURRENT_ROAD = "State St.";
+    private final Trip mTrip =
+            Trip.builder()
+                    .addDestination(mDestination)
+                    .addStep(mStep)
+                    .addDestinationTravelEstimate(mDestinationTravelEstimate)
+                    .addStepTravelEstimate(mStepTravelEstimate)
+                    .setCurrentRoad(CURRENT_ROAD)
+                    .build();
+
+    // TODO(rampara): Confirm that UiThreadTest annotation is required for test.
+//    @Before
+//    public void setUp() throws RemoteException {
+//        MockitoAnnotations.initMocks(this);
+//
+//        INavigationHost navHostStub =
+//                new INavigationHost.Stub() {
+//                    @Override
+//                    public void updateTrip(Bundleable trip) throws RemoteException {
+//                        mMockNavHost.updateTrip(trip);
+//                    }
+//
+//                    @Override
+//                    public void navigationStarted() throws RemoteException {
+//                        mMockNavHost.navigationStarted();
+//                    }
+//
+//                    @Override
+//                    public void navigationEnded() throws RemoteException {
+//                        mMockNavHost.navigationEnded();
+//                    }
+//                };
+//        when(mMockCarHost.getHost(any())).thenReturn(navHostStub.asBinder());
+//
+//        mHostDispatcher.setCarHost(mMockCarHost);
+//
+//        mNavigationManager = NavigationManager.create(mHostDispatcher);
+//    }
+
+//    @Test
+//    public void navigationStarted_sendState_navigationEnded() throws RemoteException {
+//        InOrder inOrder = inOrder(mMockNavHost);
+//
+//        mNavigationManager.setListener(mNavigationListener);
+//        mNavigationManager.navigationStarted();
+//        inOrder.verify(mMockNavHost).navigationStarted();
+//
+//        mNavigationManager.updateTrip(mTrip);
+//        inOrder.verify(mMockNavHost).updateTrip(any(Bundleable.class));
+//
+//        mNavigationManager.navigationEnded();
+//        inOrder.verify(mMockNavHost).navigationEnded();
+//    }
+
+    @Test
+    public void navigationStarted_noListenerSet() throws RemoteException {
+//        assertThrows(IllegalStateException.class, () -> mNavigationManager.navigationStarted());
+    }
+
+    // TODO(rampara): Confirm that UiThreadTest annotation is required for test.
+//    @Test
+//    public void navigationStarted_multiple() throws RemoteException {
+//
+//        mNavigationManager.setListener(mNavigationListener);
+//        mNavigationManager.navigationStarted();
+//
+//        mNavigationManager.navigationStarted();
+//        verify(mMockNavHost).navigationStarted();
+//    }
+//
+//    @Test
+//    public void navgiationEnded_multiple_not_started() throws RemoteException {
+//        mNavigationManager.navigationEnded();
+//        mNavigationManager.navigationEnded();
+//        mNavigationManager.navigationEnded();
+//        verify(mMockNavHost, never()).navigationEnded();
+//    }
+//
+//    @Test
+//    public void sendNavigationState_notStarted() throws RemoteException {
+//        assertThrows(IllegalStateException.class, () -> mNavigationManager.updateTrip(mTrip));
+//    }
+//
+//    @Test
+//    public void stopNavigation_notNavigating() throws RemoteException {
+//        mNavigationManager.setListener(mNavigationListener);
+//        mNavigationManager.getIInterface().stopNavigation(mock(IOnDoneCallback.class));
+//        verify(mNavigationListener, never()).stopNavigation();
+//    }
+//
+//    @Test
+//    public void stopNavigation_navigating_restart() throws RemoteException {
+//        InOrder inOrder = inOrder(mMockNavHost, mNavigationListener);
+//
+//        mNavigationManager.setListener(mNavigationListener);
+//        mNavigationManager.navigationStarted();
+//        inOrder.verify(mMockNavHost).navigationStarted();
+//
+//        mNavigationManager.getIInterface().stopNavigation(mock(IOnDoneCallback.class));
+//        inOrder.verify(mNavigationListener).stopNavigation();
+//
+//        mNavigationManager.navigationStarted();
+//        inOrder.verify(mMockNavHost).navigationStarted();
+//    }
+//
+//    @Test
+//    public void onAutoDriveEnabled_callsListener() {
+//        mNavigationManager.setListener(mNavigationListener);
+//        mNavigationManager.onAutoDriveEnabled();
+//
+//        verify(mNavigationListener).onAutoDriveEnabled();
+//    }
+//
+//    @Test
+//    public void onAutoDriveEnabledBeforeRegisteringListener_callsListener() {
+//        mNavigationManager.onAutoDriveEnabled();
+//        mNavigationManager.setListener(mNavigationListener);
+//
+//        verify(mNavigationListener).onAutoDriveEnabled();
+//    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/DestinationTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/DestinationTest.java
new file mode 100644
index 0000000..ba52c24
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/DestinationTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.car.app.model.CarIcon;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Destination}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DestinationTest {
+
+    @Test
+    public void createInstance() {
+        String title = "Google BVE";
+        String address = "1120 112th Ave NE";
+
+        Destination destination = Destination.builder().setName(title).setAddress(address).build();
+
+        assertThat(destination.getName().getText()).isEqualTo(title);
+        assertThat(destination.getAddress().getText()).isEqualTo(address);
+        assertThat(destination.getImage()).isNull();
+    }
+
+    @Test
+    public void emptyNameAndAddress_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> Destination.builder().setName("").setAddress("").build());
+    }
+
+    @Test
+    public void emptyNameOrAddress_allowed() {
+        Destination destination = Destination.builder().setName("name").setAddress("").build();
+        assertThat(destination.getName().getText()).isEqualTo("name");
+        assertThat(destination.getAddress().getText()).isEmpty();
+
+        destination = Destination.builder().setName(null).setAddress("address").build();
+        assertThat(destination.getAddress().getText()).isEqualTo("address");
+        assertThat(destination.getName()).isNull();
+    }
+
+    @Test
+    public void invalidCarIcon_throws() {
+        Uri.Builder builder = new Uri.Builder();
+        builder.scheme(ContentResolver.SCHEME_CONTENT);
+        builder.appendPath("foo/bar");
+        Uri iconUri = builder.build();
+        CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Destination.builder().setName("hello").setAddress("world").setImage(carIcon));
+    }
+
+    @Test
+    public void validate_hashcodeAndEquals() {
+        Destination destination1 = Destination.builder().setName("name").setAddress(
+                "address").build();
+        Destination destination2 = Destination.builder().setName("name").setAddress(
+                "address1").build();
+        Destination destination3 = Destination.builder().setName("name2").setAddress(
+                "address").build();
+        Destination destination4 = Destination.builder().setName("name").setAddress(
+                "address").build();
+
+        assertThat(destination1.hashCode()).isNotEqualTo(destination2.hashCode());
+        assertThat(destination1).isNotEqualTo(destination2);
+        assertThat(destination1.hashCode()).isNotEqualTo(destination3.hashCode());
+        assertThat(destination1).isNotEqualTo(destination3);
+        assertThat(destination1.hashCode()).isEqualTo(destination4.hashCode());
+        assertThat(destination1).isEqualTo(destination4);
+    }
+
+    @Test
+    public void equals() {
+        Destination destination = Destination.builder().setName("name").setAddress(
+                "address").build();
+
+        assertThat(destination)
+                .isEqualTo(Destination.builder().setName("name").setAddress("address").build());
+    }
+
+    @Test
+    public void notEquals_differentName() {
+        Destination destination = Destination.builder().setName("name").setAddress(
+                "address").build();
+
+        assertThat(destination)
+                .isNotEqualTo(Destination.builder().setName("Rafael").setAddress(
+                        "address").build());
+    }
+
+    @Test
+    public void notEquals_differentAddress() {
+        Destination destination = Destination.builder().setName("name").setAddress(
+                "address").build();
+
+        assertThat(destination)
+                .isNotEqualTo(Destination.builder().setName("name").setAddress(
+                        "123 main st.").build());
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneDirectionTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneDirectionTest.java
new file mode 100644
index 0000000..3f17b37
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneDirectionTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static androidx.car.app.navigation.model.LaneDirection.SHAPE_NORMAL_LEFT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link LaneDirection}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LaneDirectionTest {
+
+    @Test
+    public void createInstance() {
+        int shape = SHAPE_NORMAL_LEFT;
+        LaneDirection laneDirection = LaneDirection.create(shape, true);
+
+        assertThat(shape).isEqualTo(laneDirection.getShape());
+        assertThat(laneDirection.isHighlighted()).isTrue();
+    }
+
+    @Test
+    public void equals() {
+        LaneDirection laneDirection = LaneDirection.create(SHAPE_NORMAL_LEFT, true);
+        assertThat(LaneDirection.create(SHAPE_NORMAL_LEFT, true)).isEqualTo(laneDirection);
+    }
+
+    @Test
+    public void notEquals_differentShape() {
+        LaneDirection laneDirection = LaneDirection.create(SHAPE_NORMAL_LEFT, true);
+        assertThat(LaneDirection.create(LaneDirection.SHAPE_STRAIGHT, true))
+                .isNotEqualTo(laneDirection);
+    }
+
+    @Test
+    public void notEquals_differentHighlighted() {
+        LaneDirection laneDirection = LaneDirection.create(SHAPE_NORMAL_LEFT, true);
+        assertThat(LaneDirection.create(SHAPE_NORMAL_LEFT, false)).isNotEqualTo(laneDirection);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneTest.java
new file mode 100644
index 0000000..8d4a1197
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static androidx.car.app.navigation.model.LaneDirection.SHAPE_NORMAL_LEFT;
+import static androidx.car.app.navigation.model.LaneDirection.SHAPE_SHARP_LEFT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Lane}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LaneTest {
+
+    @Test
+    public void createInstance() {
+        LaneDirection laneDirection1 = LaneDirection.create(SHAPE_SHARP_LEFT, true);
+        LaneDirection laneDirection2 = LaneDirection.create(SHAPE_NORMAL_LEFT, false);
+        Lane lane = Lane.builder().addDirection(laneDirection1).addDirection(
+                laneDirection2).build();
+
+        assertThat(lane.getDirections()).hasSize(2);
+        assertThat(laneDirection1).isEqualTo(lane.getDirections().get(0));
+        assertThat(laneDirection2).isEqualTo(lane.getDirections().get(1));
+    }
+
+    @Test
+    public void clearDirections() {
+        LaneDirection laneDirection1 = LaneDirection.create(SHAPE_SHARP_LEFT, true);
+        LaneDirection laneDirection2 = LaneDirection.create(SHAPE_NORMAL_LEFT, false);
+        Lane lane =
+                Lane.builder()
+                        .addDirection(laneDirection1)
+                        .addDirection(laneDirection2)
+                        .clearDirections()
+                        .build();
+
+        assertThat(lane.getDirections()).hasSize(0);
+    }
+
+    @Test
+    public void equals() {
+        LaneDirection laneDirection = LaneDirection.create(SHAPE_SHARP_LEFT, true);
+        Lane lane = Lane.builder().addDirection(laneDirection).build();
+
+        assertThat(Lane.builder().addDirection(laneDirection).build()).isEqualTo(lane);
+    }
+
+    @Test
+    public void notEquals_differentDirections() {
+        LaneDirection laneDirection = LaneDirection.create(SHAPE_SHARP_LEFT, true);
+        Lane lane = Lane.builder().addDirection(laneDirection).build();
+
+        assertThat(Lane.builder().addDirection(laneDirection).addDirection(laneDirection).build())
+                .isNotEqualTo(lane);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/ManeuverTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/ManeuverTest.java
new file mode 100644
index 0000000..390b2ee
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/ManeuverTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_LEFT;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.car.app.model.CarIcon;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Maneuver}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ManeuverTest {
+
+    @Test
+    public void createInstance_non_roundabout() {
+        int type = TYPE_STRAIGHT;
+
+        Maneuver maneuver = Maneuver.builder(type).setIcon(CarIcon.APP_ICON).build();
+        assertThat(type).isEqualTo(maneuver.getType());
+        assertThat(CarIcon.APP_ICON).isEqualTo(maneuver.getIcon());
+    }
+
+    @Test
+    public void createInstance_non_roundabout_invalid_type() {
+        int typeHigh = 1000;
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Maneuver.builder(typeHigh).setIcon(CarIcon.APP_ICON).build());
+
+        int typeLow = -1;
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Maneuver.builder(typeLow).setIcon(CarIcon.APP_ICON).build());
+    }
+
+    @Test
+    public void createInstance_non_roundabout_roundabout_type() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW).setIcon(
+                        CarIcon.APP_ICON).build());
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW).setIcon(
+                        CarIcon.APP_ICON).build());
+        assertThrows(IllegalArgumentException.class,
+                () -> Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE)
+                        .setIcon(CarIcon.APP_ICON)
+                        .build());
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE)
+                        .setIcon(CarIcon.APP_ICON)
+                        .build());
+    }
+
+    @Test
+    public void createInstance_roundabout_only_exit_number() {
+        int type = TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW;
+        int roundaboutExitNumber = 2;
+
+        Maneuver maneuver =
+                Maneuver.builder(type)
+                        .setRoundaboutExitNumber(roundaboutExitNumber)
+                        .setIcon(CarIcon.APP_ICON)
+                        .build();
+        assertThat(type).isEqualTo(maneuver.getType());
+        assertThat(roundaboutExitNumber).isEqualTo(maneuver.getRoundaboutExitNumber());
+        assertThat(CarIcon.APP_ICON).isEqualTo(maneuver.getIcon());
+    }
+
+    @Test
+    public void createInstance_roundabout_invalid_type() {
+        int typeHigh = 1000;
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        Maneuver.builder(typeHigh)
+                                .setRoundaboutExitNumber(1)
+                                .setIcon(CarIcon.APP_ICON)
+                                .build());
+
+        int typeLow = -1;
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        Maneuver.builder(typeLow).setRoundaboutExitNumber(1).setIcon(
+                                CarIcon.APP_ICON).build());
+    }
+
+    @Test
+    public void createInstance_roundabout_non_roundabout_type() {
+        int type = TYPE_STRAIGHT;
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Maneuver.builder(type).setRoundaboutExitNumber(1).setIcon(
+                        CarIcon.APP_ICON).build());
+    }
+
+    @Test
+    public void createInstance_roundabout_invalid_exit() {
+        int type = TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW;
+        int roundaboutExitNumber = 0;
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        Maneuver.builder(type)
+                                .setRoundaboutExitNumber(roundaboutExitNumber)
+                                .setIcon(CarIcon.APP_ICON)
+                                .build());
+    }
+
+    @Test
+    public void createInstance_roundabout_with_angle() {
+        int type = TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE;
+        int roundaboutExitNumber = 3;
+        int roundaboutExitAngle = 270;
+
+        Maneuver maneuver =
+                Maneuver.builder(type)
+                        .setRoundaboutExitNumber(roundaboutExitNumber)
+                        .setRoundaboutExitAngle(roundaboutExitAngle)
+                        .setIcon(CarIcon.APP_ICON)
+                        .build();
+        assertThat(type).isEqualTo(maneuver.getType());
+        assertThat(roundaboutExitNumber).isEqualTo(maneuver.getRoundaboutExitNumber());
+        assertThat(roundaboutExitAngle).isEqualTo(maneuver.getRoundaboutExitAngle());
+        assertThat(CarIcon.APP_ICON).isEqualTo(maneuver.getIcon());
+    }
+
+    @Test
+    public void createInstance_roundabout_with_angle_invalid_type() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        Maneuver.builder(TYPE_STRAIGHT)
+                                .setRoundaboutExitNumber(1)
+                                .setRoundaboutExitAngle(1)
+                                .setIcon(CarIcon.APP_ICON)
+                                .build());
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        Maneuver.builder(TYPE_ROUNDABOUT_ENTER)
+                                .setRoundaboutExitNumber(1)
+                                .setRoundaboutExitAngle(1)
+                                .setIcon(CarIcon.APP_ICON)
+                                .build());
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW)
+                                .setRoundaboutExitNumber(1)
+                                .setRoundaboutExitAngle(1)
+                                .setIcon(CarIcon.APP_ICON)
+                                .build());
+    }
+
+    @Test
+    public void createInstance_invalid_carIcon() {
+        Uri.Builder builder = new Uri.Builder();
+        builder.scheme(ContentResolver.SCHEME_CONTENT);
+        builder.appendPath("foo/bar");
+        Uri iconUri = builder.build();
+        CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Maneuver.builder(TYPE_STRAIGHT).setIcon(carIcon).build());
+    }
+
+    @Test
+    public void equals() {
+        Maneuver maneuver = Maneuver.builder(TYPE_STRAIGHT).setIcon(CarIcon.APP_ICON).build();
+
+        assertThat(Maneuver.builder(TYPE_STRAIGHT).setIcon(CarIcon.APP_ICON).build())
+                .isEqualTo(maneuver);
+    }
+
+    @Test
+    public void notEquals_differentType() {
+        Maneuver maneuver = Maneuver.builder(TYPE_DESTINATION_LEFT).setIcon(
+                CarIcon.APP_ICON).build();
+
+        assertThat(Maneuver.builder(TYPE_STRAIGHT).setIcon(CarIcon.APP_ICON).build())
+                .isNotEqualTo(maneuver);
+    }
+
+    @Test
+    public void notEquals_differentImage() {
+        Maneuver maneuver = Maneuver.builder(TYPE_DESTINATION_LEFT).setIcon(
+                CarIcon.APP_ICON).build();
+
+        assertThat(Maneuver.builder(TYPE_DESTINATION_LEFT).setIcon(CarIcon.ALERT).build())
+                .isNotEqualTo(maneuver);
+    }
+
+    @Test
+    public void notEquals_differentRoundaboutExit() {
+        Maneuver maneuver =
+                Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW)
+                        .setRoundaboutExitNumber(1)
+                        .setIcon(CarIcon.APP_ICON)
+                        .build();
+
+        assertThat(
+                Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW)
+                        .setRoundaboutExitNumber(2)
+                        .setIcon(CarIcon.APP_ICON)
+                        .build())
+                .isNotEqualTo(maneuver);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/MessageInfoTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/MessageInfoTest.java
new file mode 100644
index 0000000..1be5851
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/MessageInfoTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.car.app.model.CarIcon;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link MessageInfoTest}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MessageInfoTest {
+
+    @Test
+    public void invalidCarIcon_throws() {
+        Uri.Builder builder = new Uri.Builder();
+        builder.scheme(ContentResolver.SCHEME_CONTENT);
+        builder.appendPath("foo/bar");
+        Uri iconUri = builder.build();
+        CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> MessageInfo.builder("Message").setImage(carIcon));
+    }
+
+    /** Tests basic construction of a template with a minimal data. */
+    @Test
+    public void createMinimalInstance() {
+        MessageInfo messageInfo = MessageInfo.builder("Message").build();
+        assertThat(messageInfo.getTitle().getText()).isEqualTo("Message");
+        assertThat(messageInfo.getText()).isNull();
+        assertThat(messageInfo.getImage()).isNull();
+    }
+
+    /** Tests construction of a template with all data. */
+    @Test
+    public void createFullInstance() {
+        MessageInfo messageInfo =
+                MessageInfo.builder("Message").setImage(CarIcon.APP_ICON).setText(
+                        "Secondary").build();
+        assertThat(messageInfo.getTitle().getText()).isEqualTo("Message");
+        assertThat(messageInfo.getText().getText()).isEqualTo("Secondary");
+        assertThat(messageInfo.getImage()).isEqualTo(CarIcon.APP_ICON);
+    }
+
+    @Test
+    public void no_message_throws() {
+        assertThrows(NullPointerException.class, () -> MessageInfo.builder(null));
+    }
+
+    @Test
+    public void equals() {
+        final String title = "Primary";
+        final String text = "Secondary";
+
+        MessageInfo messageInfo =
+                MessageInfo.builder(title).setText(text).setImage(CarIcon.APP_ICON).build();
+
+        assertThat(messageInfo)
+                .isEqualTo(MessageInfo.builder(title).setText(text).setImage(
+                        CarIcon.APP_ICON).build());
+    }
+
+    @Test
+    public void notEquals() {
+        final String title = "Primary";
+        final String text = "Secondary";
+
+        MessageInfo messageInfo =
+                MessageInfo.builder(title).setText(text).setImage(CarIcon.APP_ICON).build();
+
+        assertThat(messageInfo)
+                .isNotEqualTo(
+                        MessageInfo.builder("Not Primary").setText(text).setImage(
+                                CarIcon.APP_ICON).build());
+
+        assertThat(messageInfo)
+                .isNotEqualTo(
+                        MessageInfo.builder(title).setText("Not Secondary").setImage(
+                                CarIcon.APP_ICON).build());
+
+        assertThat(messageInfo)
+                .isNotEqualTo(MessageInfo.builder(title).setText(text).setImage(
+                        CarIcon.ERROR).build());
+    }
+
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/NavigationTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
new file mode 100644
index 0000000..9c3724c
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
@@ -0,0 +1,451 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static androidx.car.app.TestUtils.createDateTimeWithZone;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Distance;
+import androidx.car.app.utils.Logger;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeUnit;
+
+/** Tests for {@link NavigationTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NavigationTemplateTest {
+    private final ActionStrip mActionStrip =
+            ActionStrip.builder().addAction(TestUtils.createAction("test", null)).build();
+    private final Maneuver mManeuver =
+            Maneuver.builder(Maneuver.TYPE_FERRY_BOAT).setIcon(CarIcon.APP_ICON).build();
+    private final Step mCurrentStep =
+            Step.builder("Go Straight").setManeuver(mManeuver).setRoad("405").build();
+    private final Distance mCurrentDistance =
+            Distance.create(/* displayDistance= */ 100, Distance.UNIT_METERS);
+
+    @Test
+    public void noActionStrip_throws() {
+        assertThrows(IllegalStateException.class, () -> NavigationTemplate.builder().build());
+    }
+
+    /** Tests basic construction of a template with a minimal data. */
+    @Test
+    public void createMinimalInstance() {
+        NavigationTemplate template =
+                NavigationTemplate.builder()
+                        .setNavigationInfo(
+                                RoutingInfo.builder().setCurrentStep(mCurrentStep,
+                                        mCurrentDistance).build())
+                        .setActionStrip(mActionStrip)
+                        .build();
+        RoutingInfo routingInfo = (RoutingInfo) template.getNavigationInfo();
+        assertThat(routingInfo.getCurrentStep()).isEqualTo(mCurrentStep);
+        assertThat(routingInfo.getNextStep()).isNull();
+        assertThat(template.getBackgroundColor()).isNull();
+        assertThat(template.getDestinationTravelEstimate()).isNull();
+        assertThat(template.getActionStrip()).isEqualTo(mActionStrip);
+    }
+
+    /** Tests construction of a template with all data. */
+    @Test
+    public void createFullInstance() {
+        Maneuver nextManeuver =
+                Maneuver.builder(Maneuver.TYPE_U_TURN_LEFT).setIcon(CarIcon.APP_ICON).build();
+        Step nextStep = Step.builder("Turn Around").setManeuver(nextManeuver).setRoad(
+                "520").build();
+
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(
+                        Distance.create(/* displayDistance= */ 20, Distance.UNIT_METERS),
+                        TimeUnit.HOURS.toSeconds(1),
+                        createDateTimeWithZone("2020-05-14T19:57:00-07:00", "US/Pacific"));
+        NavigationTemplate template =
+                NavigationTemplate.builder()
+                        .setNavigationInfo(
+                                RoutingInfo.builder()
+                                        .setCurrentStep(mCurrentStep, mCurrentDistance)
+                                        .setNextStep(nextStep)
+                                        .build())
+                        .setBackgroundColor(CarColor.BLUE)
+                        .setDestinationTravelEstimate(travelEstimate)
+                        .setActionStrip(mActionStrip)
+                        .build();
+        RoutingInfo routingInfo = (RoutingInfo) template.getNavigationInfo();
+        assertThat(routingInfo.getCurrentStep()).isEqualTo(mCurrentStep);
+        assertThat(routingInfo.getCurrentDistance()).isEqualTo(mCurrentDistance);
+        assertThat(routingInfo.getNextStep()).isEqualTo(nextStep);
+        assertThat(template.getBackgroundColor()).isEqualTo(CarColor.BLUE);
+        assertThat(template.getDestinationTravelEstimate()).isEqualTo(travelEstimate);
+        assertThat(template.getActionStrip()).isEqualTo(mActionStrip);
+    }
+
+    @Test
+    public void validate_isRefresh() {
+        Logger logger = message -> {
+        };
+
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(
+                        Distance.create(/* displayDistance= */ 20, Distance.UNIT_METERS),
+                        TimeUnit.HOURS.toSeconds(1),
+                        createDateTimeWithZone("2020-05-14T19:57:00-07:00", "US/Pacific"));
+
+        Step currentStep =
+                Step.builder("Hop on a ferry")
+                        .addLane(
+                                Lane.builder()
+                                        .addDirection(LaneDirection.create(
+                                                LaneDirection.SHAPE_NORMAL_LEFT, false))
+                                        .build())
+                        .setLanesImage(CarIcon.ALERT)
+                        .build();
+        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+
+        NavigationTemplate reroutingTemplate =
+                NavigationTemplate.builder()
+                        .setNavigationInfo(RoutingInfo.builder().setIsLoading(true).build())
+                        .setActionStrip(mActionStrip)
+                        .build();
+
+        NavigationTemplate navigatingTemplate =
+                NavigationTemplate.builder()
+                        .setNavigationInfo(
+                                RoutingInfo.builder()
+                                        .setCurrentStep(currentStep, currentDistance)
+                                        .setJunctionImage(CarIcon.ALERT)
+                                        .setNextStep(currentStep)
+                                        .build())
+                        .setActionStrip(mActionStrip)
+                        .setDestinationTravelEstimate(travelEstimate)
+                        .setBackgroundColor(CarColor.BLUE)
+                        .build();
+
+        NavigationTemplate arrivedTemplate =
+                NavigationTemplate.builder()
+                        .setNavigationInfo(MessageInfo.builder("Arrived!").setText(
+                                "name\naddress").build())
+                        .setActionStrip(mActionStrip)
+                        .build();
+
+        assertThat(navigatingTemplate.isRefresh(reroutingTemplate, logger)).isTrue();
+        assertThat(arrivedTemplate.isRefresh(navigatingTemplate, logger)).isTrue();
+        assertThat(reroutingTemplate.isRefresh(arrivedTemplate, logger)).isTrue();
+    }
+
+    @Test
+    public void equals() {
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(
+                        Distance.create(/* displayDistance= */ 20, Distance.UNIT_METERS),
+                        TimeUnit.HOURS.toSeconds(1),
+                        createDateTimeWithZone("2020-05-14T19:57:00-07:00", "US/Pacific"));
+
+        Step currentStep =
+                Step.builder("Hop on a ferry")
+                        .addLane(
+                                Lane.builder()
+                                        .addDirection(LaneDirection.create(
+                                                LaneDirection.SHAPE_NORMAL_LEFT, false))
+                                        .build())
+                        .setLanesImage(CarIcon.ALERT)
+                        .build();
+        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+
+        NavigationTemplate template =
+                NavigationTemplate.builder()
+                        .setActionStrip(mActionStrip)
+                        .setDestinationTravelEstimate(travelEstimate)
+                        .setNavigationInfo(
+                                RoutingInfo.builder()
+                                        .setCurrentStep(currentStep, currentDistance)
+                                        .setJunctionImage(CarIcon.ALERT)
+                                        .setNextStep(currentStep)
+                                        .build())
+                        .setBackgroundColor(CarColor.BLUE)
+                        .build();
+
+        assertThat(template)
+                .isEqualTo(
+                        NavigationTemplate.builder()
+                                .setActionStrip(mActionStrip)
+                                .setDestinationTravelEstimate(travelEstimate)
+                                .setNavigationInfo(
+                                        RoutingInfo.builder()
+                                                .setCurrentStep(currentStep, currentDistance)
+                                                .setJunctionImage(CarIcon.ALERT)
+                                                .setNextStep(currentStep)
+                                                .build())
+                                .setBackgroundColor(CarColor.BLUE)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentActionStrip() {
+        NavigationTemplate template = NavigationTemplate.builder().setActionStrip(
+                mActionStrip).build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        NavigationTemplate.builder()
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(
+                                                TestUtils.createAction("title2", null)).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentTravelEstimate() {
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(
+                        Distance.create(/* displayDistance= */ 20, Distance.UNIT_METERS),
+                        TimeUnit.HOURS.toSeconds(1),
+                        createDateTimeWithZone("2020-05-14T19:57:00-07:00", "US/Pacific"));
+
+        NavigationTemplate template =
+                NavigationTemplate.builder()
+                        .setActionStrip(mActionStrip)
+                        .setDestinationTravelEstimate(travelEstimate)
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        NavigationTemplate.builder()
+                                .setActionStrip(mActionStrip)
+                                .setDestinationTravelEstimate(
+                                        TravelEstimate.create(
+                                                Distance.create(/* displayDistance= */ 21000,
+                                                        Distance.UNIT_METERS),
+                                                TimeUnit.HOURS.toSeconds(1),
+                                                createDateTimeWithZone("2020-05-14T19:57:00-07:00",
+                                                        "US/Pacific")))
+
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentCurrentStep() {
+        Step currentStep =
+                Step.builder("Hop on a ferry")
+                        .addLane(
+                                Lane.builder()
+                                        .addDirection(LaneDirection.create(
+                                                LaneDirection.SHAPE_NORMAL_LEFT, false))
+                                        .build())
+                        .setLanesImage(CarIcon.APP_ICON)
+                        .build();
+        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+
+        NavigationTemplate template =
+                NavigationTemplate.builder()
+                        .setActionStrip(mActionStrip)
+                        .setNavigationInfo(
+                                RoutingInfo.builder().setCurrentStep(currentStep,
+                                        currentDistance).build())
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(NavigationTemplate.builder()
+                        .setActionStrip(mActionStrip)
+                        .setNavigationInfo(RoutingInfo.builder()
+                                .setCurrentStep(Step.builder("do a back flip")
+                                                .addLane(Lane.builder()
+                                                        .addDirection(LaneDirection.create(
+                                                                LaneDirection.SHAPE_NORMAL_LEFT,
+                                                                false))
+                                                        .build())
+                                                .setLanesImage(CarIcon.APP_ICON)
+                                                .build(),
+                                        currentDistance)
+                                .build())
+                        .build());
+    }
+
+    @Test
+    public void notEquals_differentCurrentDistance() {
+        Step currentStep = Step.builder("Hop on a ferry")
+                .addLane(Lane.builder()
+                        .addDirection(LaneDirection.create(
+                                LaneDirection.SHAPE_NORMAL_LEFT, false))
+                        .build())
+                .setLanesImage(CarIcon.APP_ICON)
+                .build();
+        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+
+        NavigationTemplate template =
+                NavigationTemplate.builder()
+                        .setActionStrip(mActionStrip)
+                        .setNavigationInfo(
+                                RoutingInfo.builder().setCurrentStep(currentStep,
+                                        currentDistance).build())
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        NavigationTemplate.builder()
+                                .setActionStrip(mActionStrip)
+                                .setNavigationInfo(
+                                        RoutingInfo.builder()
+                                                .setCurrentStep(
+                                                        currentStep,
+                                                        Distance.create(/* displayDistance= */ 200,
+                                                                Distance.UNIT_METERS))
+                                                .build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentJunctionImage() {
+        Step currentStep = Step.builder("Hop on a ferry")
+                .addLane(Lane.builder()
+                        .addDirection(LaneDirection.create(
+                                LaneDirection.SHAPE_NORMAL_LEFT, false))
+                        .build())
+                .setLanesImage(CarIcon.ALERT)
+                .build();
+        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+
+        NavigationTemplate template =
+                NavigationTemplate.builder()
+                        .setActionStrip(mActionStrip)
+                        .setNavigationInfo(
+                                RoutingInfo.builder()
+                                        .setCurrentStep(currentStep, currentDistance)
+                                        .setJunctionImage(CarIcon.ALERT)
+                                        .setNextStep(currentStep)
+                                        .build())
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        NavigationTemplate.builder()
+                                .setActionStrip(mActionStrip)
+                                .setNavigationInfo(
+                                        RoutingInfo.builder()
+                                                .setCurrentStep(currentStep, currentDistance)
+                                                .setJunctionImage(CarIcon.ERROR)
+                                                .setNextStep(currentStep)
+                                                .build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentNextStep() {
+        Step currentStep = Step.builder("Hop on a ferry")
+                .addLane(Lane.builder()
+                        .addDirection(LaneDirection.create(
+                                LaneDirection.SHAPE_NORMAL_LEFT, false))
+                        .build())
+                .setLanesImage(CarIcon.ALERT)
+                .build();
+        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+
+        NavigationTemplate template =
+                NavigationTemplate.builder()
+                        .setActionStrip(mActionStrip)
+                        .setNavigationInfo(
+                                RoutingInfo.builder()
+                                        .setCurrentStep(currentStep, currentDistance)
+                                        .setNextStep(currentStep)
+                                        .build())
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(NavigationTemplate.builder()
+                        .setActionStrip(mActionStrip)
+                        .setNavigationInfo(RoutingInfo.builder()
+                                .setCurrentStep(currentStep, currentDistance)
+                                .setNextStep(Step.builder("Do a backflip")
+                                        .addLane(Lane.builder()
+                                                .addDirection(LaneDirection.create(
+                                                        LaneDirection.SHAPE_NORMAL_LEFT,
+                                                        false))
+                                                .build())
+                                        .setLanesImage(CarIcon.ALERT)
+                                        .build())
+                                .build())
+                        .build());
+    }
+
+    @Test
+    public void notEquals_differentBackgroundColors() {
+        NavigationTemplate template =
+                NavigationTemplate.builder()
+                        .setActionStrip(mActionStrip)
+                        .setBackgroundColor(CarColor.BLUE)
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        NavigationTemplate.builder()
+                                .setActionStrip(mActionStrip)
+                                .setBackgroundColor(CarColor.GREEN)
+                                .build());
+    }
+
+    @Test
+    public void checkPermissions_hasPermissions() {
+        //TODO(rampara): Investigate failure to create ShadowPackageManager
+//        NavigationTemplate template =
+//                NavigationTemplate.builder()
+//                        .setActionStrip(mActionStrip)
+//                        .setBackgroundColor(CarColor.BLUE)
+//                        .build();
+//
+//        Context context = ApplicationProvider.getApplicationContext();
+//        PackageManager packageManager = context.getPackageManager();
+//        PackageInfo pi = new PackageInfo();
+//        pi.packageName = context.getPackageName();
+//        pi.versionCode = 1;
+//        pi.requestedPermissions = new String[]{CarAppPermission.NAVIGATION_TEMPLATES};
+//        shadowOf(packageManager).installPackage(pi);
+//
+//        // Expect that it does not throw
+//        template.checkPermissions(context);
+    }
+
+    @Test
+    public void checkPermissions_doesNotHavePermissions() {
+        NavigationTemplate template =
+                NavigationTemplate.builder()
+                        .setActionStrip(mActionStrip)
+                        .setBackgroundColor(CarColor.BLUE)
+                        .build();
+
+        assertThrows(
+                SecurityException.class,
+                () -> template.checkPermissions(ApplicationProvider.getApplicationContext()));
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
new file mode 100644
index 0000000..18a4946
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
@@ -0,0 +1,524 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.text.SpannableString;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Distance;
+import androidx.car.app.model.DistanceSpan;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.LatLng;
+import androidx.car.app.model.Metadata;
+import androidx.car.app.model.Place;
+import androidx.car.app.model.PlaceMarker;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Toggle;
+import androidx.car.app.test.R;
+import androidx.car.app.utils.Logger;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link PlaceListNavigationTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PlaceListNavigationTemplateTest {
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final DistanceSpan mDistanceSpan =
+            DistanceSpan.create(
+                    Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
+
+    @Test
+    public void createInstance_emptyList_notLoading_Throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> PlaceListNavigationTemplate.builder().setTitle("Title").build());
+
+        // Positive case
+        PlaceListNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+    }
+
+    @Test
+    public void createInstance_isLoading_hasList_Throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setIsLoading(true)
+                                .setItemList(ItemList.builder().build())
+                                .build());
+    }
+
+    @Test
+    public void addList_selectable_throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(TestUtils.createItemListWithDistanceSpan(6, true,
+                                        mDistanceSpan))
+                                .build());
+
+        // Positive cases.
+        PlaceListNavigationTemplate.builder()
+                .setTitle("Title")
+                .setItemList(TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                .build();
+    }
+
+    @Test
+    public void addList_moreThanMaxTexts_throws() {
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+        Row rowExceedsMaxTexts =
+                Row.builder().setTitle(title).addText("text1").addText("text2").addText(
+                        "text3").build();
+        Row rowMeetingMaxTexts =
+                Row.builder().setTitle(title).addText("text1").addText("text2").build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(ItemList.builder().addItem(rowExceedsMaxTexts).build())
+                                .build());
+
+        // Positive cases.
+        PlaceListNavigationTemplate.builder()
+                .setTitle("Title")
+                .setItemList(ItemList.builder().addItem(rowMeetingMaxTexts).build())
+                .build();
+    }
+
+    @Test
+    public void addList_hasToggle_throws() {
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+        Row rowWithToggle =
+                Row.builder().setTitle(title).setToggle(Toggle.builder(isChecked -> {
+                }).build()).build();
+        Row rowMeetingRestrictions =
+                Row.builder().setTitle(title).addText("text1").addText("text2").build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(ItemList.builder().addItem(rowWithToggle).build())
+                                .build());
+
+        // Positive cases.
+        PlaceListNavigationTemplate.builder()
+                .setTitle("Title")
+                .setItemList(ItemList.builder().addItem(rowMeetingRestrictions).build())
+                .build();
+    }
+
+    @Test
+    public void createInstance_noHeaderTitleOrAction_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        PlaceListNavigationTemplate.builder().setItemList(
+                                ItemList.builder().build()).build());
+
+        // Positive cases.
+        PlaceListNavigationTemplate.builder()
+                .setTitle("Title")
+                .setItemList(ItemList.builder().build())
+                .build();
+        PlaceListNavigationTemplate.builder()
+                .setHeaderAction(Action.BACK)
+                .setItemList(ItemList.builder().build())
+                .build();
+    }
+
+    @Test
+    public void createEmpty() {
+        PlaceListNavigationTemplate template =
+                PlaceListNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(ItemList.builder().build())
+                        .build();
+        assertThat(template.getItemList().getItems()).isEmpty();
+        assertThat(template.getTitle().getText()).isEqualTo("Title");
+        assertThat(template.getActionStrip()).isNull();
+    }
+
+    @Test
+    public void createInstance() {
+        String title = "title";
+        ItemList itemList = TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan);
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        PlaceListNavigationTemplate template =
+                PlaceListNavigationTemplate.builder()
+                        .setItemList(itemList)
+                        .setTitle(title)
+                        .setActionStrip(actionStrip)
+                        .build();
+        assertThat(template.getItemList()).isEqualTo(itemList);
+        assertThat(template.getActionStrip()).isEqualTo(actionStrip);
+        assertThat(template.getTitle().getText()).isEqualTo(title);
+    }
+
+    @Test
+    public void createInstance_setHeaderAction_invalidActionThrows() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListNavigationTemplate.builder()
+                                .setHeaderAction(
+                                        Action.builder().setTitle("Action").setOnClickListener(
+                                                () -> {
+                                                }).build()));
+    }
+
+    @Test
+    public void createInstance_setHeaderAction() {
+        PlaceListNavigationTemplate template =
+                PlaceListNavigationTemplate.builder()
+                        .setItemList(ItemList.builder().build())
+                        .setHeaderAction(Action.BACK)
+                        .build();
+
+        assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+    }
+
+    @Test
+    public void createInstance_notAllRowHaveDistances() {
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+        Row rowWithDistance = Row.builder().setTitle(title).build();
+        Row rowWithoutDistance = Row.builder().setTitle("Google Kir").build();
+        Row browsableRowWithoutDistance =
+                Row.builder()
+                        .setTitle("Google Kir")
+                        .setBrowsable(true)
+                        .setOnClickListener(() -> {
+                        })
+                        .build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        ItemList.builder().addItem(rowWithDistance).addItem(
+                                                rowWithoutDistance).build())
+                                .build());
+
+        // Positive cases
+        PlaceListNavigationTemplate.builder()
+                .setTitle("Title")
+                .setItemList(ItemList.builder().addItem(rowWithDistance).build())
+                .build();
+        PlaceListNavigationTemplate.builder()
+                .setTitle("Title")
+                .setItemList(
+                        ItemList.builder()
+                                .addItem(rowWithDistance)
+                                .addItem(browsableRowWithoutDistance)
+                                .build())
+                .build();
+        PlaceListNavigationTemplate.builder()
+                .setTitle("Title")
+                .setItemList(ItemList.builder().addItem(browsableRowWithoutDistance).build())
+                .build();
+    }
+
+    @Test
+    public void createInstance_rowHasBothMarkerAndImages() {
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+        Row row =
+                Row.builder()
+                        .setTitle("Google Kir")
+                        .setOnClickListener(() -> {
+                        })
+                        .setImage(CarIcon.ALERT)
+                        .setMetadata(
+                                Metadata.ofPlace(
+                                        Place.builder(LatLng.create(10.f, 10.f))
+                                                .setMarker(PlaceMarker.getDefault())
+                                                .build()))
+                        .build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        PlaceListNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(ItemList.builder().addItem(row).build()));
+    }
+
+    @Test
+    public void validate_isRefresh() {
+        Logger logger = message -> {
+        };
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+        Row.Builder row =
+                Row.builder().setTitle(title).setBrowsable(true).setOnClickListener(() -> {
+                });
+        PlaceListNavigationTemplate template =
+                PlaceListNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(ItemList.builder().addItem(row.build()).build())
+                        .build();
+
+        assertThat(template.isRefresh(template, logger)).isTrue();
+
+        // Going from loading state to new content is allowed.
+        assertThat(
+                template.isRefresh(
+                        PlaceListNavigationTemplate.builder().setTitle("Title").setIsLoading(
+                                true).build(),
+                        logger))
+                .isTrue();
+
+        // Other allowed mutable states.
+        SpannableString stringWithSpan = new SpannableString("Title");
+        stringWithSpan.setSpan(mDistanceSpan, 1, /* end= */ 2, /* flags= */ 0);
+        assertThat(template.isRefresh(PlaceListNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(ItemList.builder()
+                                .addItem(row.setOnClickListener(() -> {
+                                })
+                                        .setBrowsable(false)
+                                        .setTitle(stringWithSpan)
+                                        .setImage(CarIcon.of(
+                                                IconCompat.createWithResource(
+                                                        ApplicationProvider.getApplicationContext(),
+                                                        R.drawable.ic_test_1)))
+                                        .setMetadata(Metadata.ofPlace(
+                                                Place.builder(
+                                                        LatLng.create(
+                                                                1,
+                                                                1)).build()))
+                                        .build())
+                                .build())
+                        .setHeaderAction(Action.BACK)
+                        .setActionStrip(
+                                ActionStrip.builder().addAction(Action.APP_ICON).build())
+                        .build(),
+                logger))
+                .isTrue();
+
+        // Title updates are disallowed.
+        assertThat(
+                template.isRefresh(
+                        PlaceListNavigationTemplate.builder()
+                                .setItemList(ItemList.builder().addItem(row.build()).build())
+                                .setTitle("Title2")
+                                .build(),
+                        logger))
+                .isFalse();
+
+        // Text updates are disallowed.
+        SpannableString title2 = new SpannableString("Title2");
+        title2.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
+        assertThat(
+                template.isRefresh(
+                        PlaceListNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(ItemList.builder().addItem(
+                                        row.setTitle(title2).build()).build())
+                                .build(),
+                        logger))
+                .isFalse();
+        assertThat(
+                template.isRefresh(
+                        PlaceListNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        ItemList.builder()
+                                                .addItem(row.setTitle(title).addText(
+                                                        "Text").build())
+                                                .build())
+                                .build(),
+                        logger))
+                .isFalse();
+
+        // Additional rows are disallowed.
+        assertThat(
+                template.isRefresh(
+                        PlaceListNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        ItemList.builder().addItem(row.build()).addItem(
+                                                row.build()).build())
+                                .build(),
+                        logger))
+                .isFalse();
+
+        // Going from content to loading state is disallowed.
+        assertThat(
+                PlaceListNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setIsLoading(true)
+                        .build()
+                        .isRefresh(template, logger))
+                .isFalse();
+    }
+
+    @Test
+    public void equals() {
+        PlaceListNavigationTemplate template =
+                PlaceListNavigationTemplate.builder()
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .setHeaderAction(Action.BACK)
+                        .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+                        .setTitle("title")
+                        .build();
+
+        assertThat(template)
+                .isEqualTo(
+                        PlaceListNavigationTemplate.builder()
+                                .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+                                        mDistanceSpan))
+                                .setHeaderAction(Action.BACK)
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.BACK).build())
+                                .setTitle("title")
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentItemList() {
+        PlaceListNavigationTemplate template =
+                PlaceListNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        PlaceListNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(TestUtils.createItemListWithDistanceSpan(5, false,
+                                        mDistanceSpan))
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentHeaderAction() {
+        PlaceListNavigationTemplate template =
+                PlaceListNavigationTemplate.builder()
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .setHeaderAction(Action.BACK)
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        PlaceListNavigationTemplate.builder()
+                                .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+                                        mDistanceSpan))
+                                .setHeaderAction(Action.APP_ICON)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentActionStrip() {
+        PlaceListNavigationTemplate template =
+                PlaceListNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        PlaceListNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+                                        mDistanceSpan))
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentTitle() {
+        PlaceListNavigationTemplate template =
+                PlaceListNavigationTemplate.builder()
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .setTitle("title")
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        PlaceListNavigationTemplate.builder()
+                                .setItemList(TestUtils.createItemListWithDistanceSpan(6, false,
+                                        mDistanceSpan))
+                                .setTitle("other")
+                                .build());
+    }
+
+    @Test
+    public void checkPermissions_hasPermissions() {
+        //TODO(rampara): Investigate failure to create ShadowPackageManager
+//        PlaceListNavigationTemplate template =
+//                PlaceListNavigationTemplate.builder()
+//                        .setItemList(
+//                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+//                        .setTitle("title")
+//                        .build();
+//
+//        PackageManager packageManager = mContext.getPackageManager();
+//        PackageInfo pi = new PackageInfo();
+//        pi.packageName = mContext.getPackageName();
+//        pi.versionCode = 1;
+//        pi.requestedPermissions = new String[]{CarAppPermission.NAVIGATION_TEMPLATES};
+//        shadowOf(packageManager).installPackage(pi);
+//
+//        // Expect that it does not throw
+//        template.checkPermissions(mContext);
+    }
+
+    @Test
+    public void checkPermissions_doesNotHavePermissions() {
+        PlaceListNavigationTemplate template =
+                PlaceListNavigationTemplate.builder()
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                        .setTitle("title")
+                        .build();
+
+        assertThrows(SecurityException.class, () -> template.checkPermissions(mContext));
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
new file mode 100644
index 0000000..ac7252b
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
@@ -0,0 +1,615 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.os.RemoteException;
+import android.text.SpannableString;
+
+import androidx.car.app.TestUtils;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Distance;
+import androidx.car.app.model.DistanceSpan;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.Row;
+import androidx.car.app.test.R;
+import androidx.car.app.utils.Logger;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link RoutePreviewNavigationTemplate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RoutePreviewNavigationTemplateTest {
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private static final DistanceSpan DISTANCE =
+            DistanceSpan.create(
+                    Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
+
+    @Test
+    public void createInstance_emptyList_notLoading_Throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> RoutePreviewNavigationTemplate.builder().setTitle("Title").build());
+
+        // Positive case
+        RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+    }
+
+    @Test
+    public void createInstance_isLoading_hasList_Throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setIsLoading(true)
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .build());
+    }
+
+    @Test
+    public void addList_notSelectable_throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(TestUtils.createItemListWithDistanceSpan(3, false,
+                                DISTANCE)));
+
+        // Positive case.
+        RoutePreviewNavigationTemplate.builder()
+                .setTitle("Title")
+                .setItemList(TestUtils.createItemListWithDistanceSpan(3, true, DISTANCE));
+    }
+
+    @Test
+    public void addList_moreThanMaxTexts_throws() {
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(DISTANCE, 0, 1, 0);
+        Row rowExceedsMaxTexts =
+                Row.builder().setTitle(title).addText("text1").addText("text2").addText(
+                        "text3").build();
+        Row rowMeetingMaxTexts =
+                Row.builder().setTitle(title).addText("text1").addText("text2").build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(
+                                ItemList.builder()
+                                        .addItem(rowExceedsMaxTexts)
+                                        .setSelectable(selectedIndex -> {
+                                        })
+                                        .build()));
+
+        // Positive case.
+        RoutePreviewNavigationTemplate.builder()
+                .setTitle("Title")
+                .setItemList(
+                        ItemList.builder()
+                                .addItem(rowMeetingMaxTexts)
+                                .setSelectable(selectedIndex -> {
+                                })
+                                .build());
+    }
+
+    @Test
+    public void noHeaderTitleOrAction_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> RoutePreviewNavigationTemplate.builder().setIsLoading(true).build());
+
+        // Positive cases.
+        RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+        RoutePreviewNavigationTemplate.builder()
+                .setHeaderAction(Action.BACK)
+                .setIsLoading(true)
+                .build();
+    }
+
+    @Test
+    public void createInstance() {
+        ItemList itemList = TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE);
+        String title = "title";
+        RoutePreviewNavigationTemplate template =
+                RoutePreviewNavigationTemplate.builder()
+                        .setItemList(itemList)
+                        .setTitle(title)
+                        .setNavigateAction(
+                                Action.builder().setTitle("Navigate").setOnClickListener(() -> {
+                                }).build())
+                        .build();
+        assertThat(template.getItemList()).isEqualTo(itemList);
+        assertThat(template.getTitle().getText()).isEqualTo(title);
+    }
+
+    @Test
+    public void createInstance_setHeaderAction_invalidActionThrows() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> RoutePreviewNavigationTemplate.builder()
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .setNavigateAction(
+                                Action.builder().setTitle("Navigate").setOnClickListener(
+                                        () -> {
+                                        }).build())
+                        .setHeaderAction(
+                                Action.builder().setTitle("Action").setOnClickListener(
+                                        () -> {
+                                        }).build()));
+    }
+
+    @Test
+    public void createInstance_setHeaderAction() {
+        RoutePreviewNavigationTemplate template =
+                RoutePreviewNavigationTemplate.builder()
+                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .setNavigateAction(
+                                Action.builder().setTitle("Navigate").setOnClickListener(() -> {
+                                }).build())
+                        .setHeaderAction(Action.BACK)
+                        .build();
+
+        assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+    }
+
+    @Test
+    public void setOnNavigateAction() throws RemoteException {
+        // TODO(rampara): Confirm that UiThreadTest annotation is required for test.
+//        OnClickListener mockListener = mock(OnClickListener.class);
+//        RoutePreviewNavigationTemplate template =
+//                RoutePreviewNavigationTemplate.builder()
+//                        .setTitle("Title")
+//                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+//                        .setNavigateAction(
+//                                Action.builder().setTitle("Navigate").setOnClickListener(
+//                                        mockListener).build())
+//                        .build();
+//
+//        template.getNavigateAction()
+//                .getOnClickListener()
+//                .getListener()
+//                .onClick(mock(IOnDoneCallback.class));
+//        verify(mockListener).onClick();
+    }
+
+    @Test
+    public void createInstance_emptyNavigateAction_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(
+                                TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .build());
+
+        // Positive case
+        RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+    }
+
+    @Test
+    public void createInstance_emptyListeners_throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, false,
+                                DISTANCE))
+                        .setNavigateAction(
+                                Action.builder().setTitle("Navigate").setOnClickListener(
+                                        () -> {
+                                        }).build())
+                        .build());
+
+        // Positive case
+        RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+    }
+
+    @Test
+    public void createInstance_navigateActionNoTitle_throws() {
+        CarIcon carIcon = CarIcon.of(IconCompat.createWithResource(
+                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .setNavigateAction(Action.builder().setIcon(carIcon).setOnClickListener(
+                                () -> {
+                                }).build())
+                        .build());
+
+        // Positive case
+        RoutePreviewNavigationTemplate.builder()
+                .setTitle("Title")
+                .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                .setNavigateAction(Action.builder()
+                        .setIcon(carIcon)
+                        .setTitle("Navigate")
+                        .setOnClickListener(() -> {
+                        })
+                        .build())
+                .build();
+    }
+
+    @Test
+    public void createInstance_notAllRowsHaveTime() {
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(DISTANCE, 0, 1, 0);
+        Row rowWithTime = Row.builder().setTitle(title).build();
+        Row rowWithoutTime = Row.builder().setTitle("Google Bve").build();
+        Action navigateAction = Action.builder()
+                .setIcon(CarIcon.of(IconCompat.createWithResource(
+                        ApplicationProvider.getApplicationContext(),
+                        R.drawable.ic_test_1)))
+                .setTitle("Navigate")
+                .setOnClickListener(() -> {
+                })
+                .build();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(ItemList.builder()
+                                .addItem(rowWithTime)
+                                .addItem(rowWithoutTime)
+                                .setSelectable(index -> {
+                                })
+                                .build())
+                        .setNavigateAction(navigateAction)
+                        .build());
+
+        // Positive case
+        RoutePreviewNavigationTemplate.builder()
+                .setTitle("Title")
+                .setItemList(ItemList.builder().setSelectable(index -> {
+                }).addItem(rowWithTime).build())
+                .setNavigateAction(navigateAction)
+                .build();
+    }
+
+    @Test
+    public void validate_isRefresh() {
+        Logger logger = message -> {
+        };
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(DISTANCE, 0, 1, 0);
+        Row.Builder row = Row.builder().setTitle(title);
+        Action navigateAction = Action.builder().setTitle("Navigate").setOnClickListener(() -> {
+        }).build();
+        RoutePreviewNavigationTemplate template = RoutePreviewNavigationTemplate.builder()
+                .setTitle("Title")
+                .setItemList(ItemList.builder().addItem(row.build()).setSelectable(
+                        index -> {
+                        }).build())
+                .setNavigateAction(navigateAction)
+                .build();
+
+        assertThat(template.isRefresh(template, logger)).isTrue();
+
+        // Going from loading state to new content is allowed.
+        assertThat(template.isRefresh(
+                RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setIsLoading(true)
+                        .build(),
+                logger))
+                .isTrue();
+
+        // Other allowed mutable states.
+        SpannableString stringWithSpan = new SpannableString("Title");
+        stringWithSpan.setSpan(DISTANCE, 1, /* end= */ 2, /* flags= */ 0);
+        assertThat(template.isRefresh(
+                RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(ItemList.builder()
+                                .addItem(row.setImage(
+                                        CarIcon.of(IconCompat.createWithResource(
+                                                ApplicationProvider.getApplicationContext(),
+                                                R.drawable.ic_test_1)))
+                                        .setTitle(stringWithSpan)
+                                        .build())
+                                .setSelectable(index -> {
+                                })
+                                .build())
+                        .setHeaderAction(Action.BACK)
+                        .setNavigateAction(Action.builder().setTitle(
+                                "Navigate2").setOnClickListener(() -> {
+                                }
+                        ).build())
+                        .setActionStrip(ActionStrip.builder().addAction(Action.APP_ICON).build())
+                        .build(),
+                logger))
+                .isTrue();
+
+        // Title updates are disallowed.
+        assertThat(template.isRefresh(
+                RoutePreviewNavigationTemplate.builder()
+                        .setItemList(ItemList.builder().addItem(row.build()).setSelectable(
+                                index -> {
+                                }).build())
+                        .setTitle("Title2")
+                        .setNavigateAction(navigateAction)
+                        .build(),
+                logger))
+                .isFalse();
+
+        // Text updates are disallowed.
+        SpannableString title2 = new SpannableString("Title2");
+        title2.setSpan(DISTANCE, 0, 1, 0);
+        assertThat(
+                template.isRefresh(
+                        RoutePreviewNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        ItemList.builder()
+                                                .addItem(row.setTitle(title2).build())
+                                                .setSelectable(index -> {
+                                                })
+                                                .build())
+                                .setNavigateAction(navigateAction)
+                                .build(),
+                        logger))
+                .isFalse();
+        assertThat(
+                template.isRefresh(
+                        RoutePreviewNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        ItemList.builder()
+                                                .addItem(row.addText("Text").build())
+                                                .setSelectable(index -> {
+                                                })
+                                                .build())
+                                .setNavigateAction(navigateAction)
+                                .build(),
+                        logger))
+                .isFalse();
+
+        // Additional rows are disallowed.
+        assertThat(
+                template.isRefresh(
+                        RoutePreviewNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        ItemList.builder()
+                                                .addItem(row.build())
+                                                .addItem(row.build())
+                                                .setSelectable(index -> {
+                                                })
+                                                .build())
+                                .setNavigateAction(navigateAction)
+                                .build(),
+                        logger))
+                .isFalse();
+
+        // Going from content to loading state is disallowed.
+        assertThat(
+                RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setIsLoading(true)
+                        .build()
+                        .isRefresh(template, logger))
+                .isFalse();
+    }
+
+    @Test
+    public void equals() {
+        RoutePreviewNavigationTemplate template =
+                RoutePreviewNavigationTemplate.builder()
+                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+                        .setTitle("title")
+                        .setHeaderAction(Action.BACK)
+                        .setNavigateAction(
+                                Action.builder().setTitle("drive").setOnClickListener(() -> {
+                                }).build())
+                        .build();
+
+        assertThat(template)
+                .isEqualTo(
+                        RoutePreviewNavigationTemplate.builder()
+                                .setItemList(
+                                        TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.BACK).build())
+                                .setTitle("title")
+                                .setHeaderAction(Action.BACK)
+                                .setNavigateAction(
+                                        Action.builder().setTitle("drive").setOnClickListener(
+                                                () -> {
+                                                }).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentItemList() {
+        RoutePreviewNavigationTemplate template =
+                RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .setNavigateAction(
+                                Action.builder().setTitle("drive").setOnClickListener(() -> {
+                                }).build())
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        RoutePreviewNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        TestUtils.createItemListWithDistanceSpan(1, true, DISTANCE))
+                                .setNavigateAction(
+                                        Action.builder().setTitle("drive").setOnClickListener(
+                                                () -> {
+                                                }).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentHeaderAction() {
+        RoutePreviewNavigationTemplate template =
+                RoutePreviewNavigationTemplate.builder()
+                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .setHeaderAction(Action.BACK)
+                        .setNavigateAction(
+                                Action.builder().setTitle("drive").setOnClickListener(() -> {
+                                }).build())
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        RoutePreviewNavigationTemplate.builder()
+                                .setItemList(
+                                        TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                                .setHeaderAction(Action.APP_ICON)
+                                .setNavigateAction(
+                                        Action.builder().setTitle("drive").setOnClickListener(
+                                                () -> {
+                                                }).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentActionStrip() {
+        RoutePreviewNavigationTemplate template =
+                RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+                        .setNavigateAction(
+                                Action.builder().setTitle("drive").setOnClickListener(() -> {
+                                }).build())
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        RoutePreviewNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
+                                .setNavigateAction(
+                                        Action.builder().setTitle("drive").setOnClickListener(
+                                                () -> {
+                                                }).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentTitle() {
+        SpannableString title = new SpannableString("Title");
+        title.setSpan(DISTANCE, 0, 1, 0);
+        RoutePreviewNavigationTemplate template =
+                RoutePreviewNavigationTemplate.builder()
+                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .setTitle(title)
+                        .setNavigateAction(
+                                Action.builder().setTitle("drive").setOnClickListener(() -> {
+                                }).build())
+                        .build();
+
+        SpannableString title2 = new SpannableString("Title2");
+        title2.setSpan(DISTANCE, 0, 1, 0);
+        assertThat(template)
+                .isNotEqualTo(
+                        RoutePreviewNavigationTemplate.builder()
+                                .setItemList(
+                                        TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                                .setTitle(title2)
+                                .setNavigateAction(
+                                        Action.builder().setTitle("drive").setOnClickListener(
+                                                () -> {
+                                                }).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentNavigateAction() {
+        RoutePreviewNavigationTemplate template =
+                RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .setNavigateAction(
+                                Action.builder().setTitle("drive").setOnClickListener(() -> {
+                                }).build())
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        RoutePreviewNavigationTemplate.builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                                .setNavigateAction(
+                                        Action.builder().setTitle("stop").setOnClickListener(() -> {
+                                        }).build())
+                                .build());
+    }
+
+    @Test
+    public void checkPermissions_hasPermissions() {
+        //TODO(rampara): Investigate failure to create ShadowPackageManager
+//        RoutePreviewNavigationTemplate template =
+//                RoutePreviewNavigationTemplate.builder()
+//                        .setTitle("Title")
+//                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+//                        .setNavigateAction(
+//                                Action.builder().setTitle("drive").setOnClickListener(() -> {
+//                                }).build())
+//                        .build();
+//
+//        PackageManager packageManager = mContext.getPackageManager();
+//        PackageInfo pi = new PackageInfo();
+//        pi.packageName = mContext.getPackageName();
+//        pi.versionCode = 1;
+//        pi.requestedPermissions = new String[]{CarAppPermission.NAVIGATION_TEMPLATES};
+//        shadowOf(packageManager).installPackage(pi);
+//
+//        // Expect that it does not throw
+//        template.checkPermissions(mContext);
+    }
+
+    @Test
+    public void checkPermissions_doesNotHavePermissions() {
+        RoutePreviewNavigationTemplate template =
+                RoutePreviewNavigationTemplate.builder()
+                        .setTitle("Title")
+                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .setNavigateAction(
+                                Action.builder().setTitle("drive").setOnClickListener(() -> {
+                                }).build())
+                        .build();
+
+        assertThrows(SecurityException.class, () -> template.checkPermissions(mContext));
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutingInfoTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutingInfoTest.java
new file mode 100644
index 0000000..58ea716
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutingInfoTest.java
@@ -0,0 +1,266 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Distance;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link RoutingInfoTest}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RoutingInfoTest {
+
+    private final Maneuver mManeuver =
+            Maneuver.builder(Maneuver.TYPE_FERRY_BOAT).setIcon(CarIcon.APP_ICON).build();
+    private final Step mCurrentStep =
+            Step.builder("Go Straight").setManeuver(mManeuver).setRoad("405").build();
+    private final Distance mCurrentDistance =
+            Distance.create(/* displayDistance= */ 100, Distance.UNIT_METERS);
+
+    @Test
+    public void noCurrentStep_throws() {
+        assertThrows(IllegalStateException.class, () -> RoutingInfo.builder().build());
+    }
+
+    @Test
+    public void isLoading_throws_when_not_empty() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> RoutingInfo.builder()
+                        .setIsLoading(true)
+                        .setCurrentStep(mCurrentStep, mCurrentDistance)
+                        .build());
+    }
+
+    @Test
+    public void invalidCarIcon_throws() {
+        Uri.Builder builder = new Uri.Builder();
+        builder.scheme(ContentResolver.SCHEME_CONTENT);
+        builder.appendPath("foo/bar");
+        Uri iconUri = builder.build();
+        CarIcon carIcon = CarIcon.of(IconCompat.createWithContentUri(iconUri));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> RoutingInfo.builder().setJunctionImage(carIcon));
+    }
+
+    /** Tests basic construction of a template with a minimal data. */
+    @Test
+    public void createMinimalInstance() {
+        RoutingInfo routingInfo =
+                RoutingInfo.builder().setCurrentStep(mCurrentStep, mCurrentDistance).build();
+        assertThat(routingInfo.getCurrentStep()).isEqualTo(mCurrentStep);
+        assertThat(routingInfo.getNextStep()).isNull();
+    }
+
+    /** Tests construction of a template with all data. */
+    @Test
+    public void createFullInstance() {
+        Maneuver nextManeuver =
+                Maneuver.builder(Maneuver.TYPE_U_TURN_LEFT).setIcon(CarIcon.APP_ICON).build();
+        Step nextStep = Step.builder("Turn Around").setManeuver(nextManeuver).setRoad(
+                "520").build();
+
+        RoutingInfo routingInfo = RoutingInfo.builder()
+                .setCurrentStep(mCurrentStep, mCurrentDistance)
+                .setNextStep(nextStep)
+                .build();
+        assertThat(routingInfo.getCurrentStep()).isEqualTo(mCurrentStep);
+        assertThat(routingInfo.getCurrentDistance()).isEqualTo(mCurrentDistance);
+        assertThat(routingInfo.getNextStep()).isEqualTo(nextStep);
+    }
+
+    @Test
+    public void laneInfo_set_no_lanesImage_throws() {
+        Step currentStep =
+                Step.builder("Hop on a ferry")
+                        .addLane(Lane.builder()
+                                .addDirection(LaneDirection.create(
+                                        LaneDirection.SHAPE_NORMAL_LEFT, false))
+                                .build())
+                        .build();
+        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+        assertThrows(
+                IllegalStateException.class,
+                () -> RoutingInfo.builder().setCurrentStep(currentStep, currentDistance).build());
+    }
+
+    @Test
+    public void laneInfo_set_with_lanesImage_doesnt_throws() {
+        Step currentStep =
+                Step.builder("Hop on a ferry")
+                        .addLane(Lane.builder()
+                                .addDirection(LaneDirection.create(
+                                        LaneDirection.SHAPE_NORMAL_LEFT, false))
+                                .build())
+                        .setLanesImage(CarIcon.APP_ICON)
+                        .build();
+        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+        RoutingInfo.builder().setCurrentStep(currentStep, currentDistance).build();
+    }
+
+    @Test
+    public void equals() {
+        Step currentStep =
+                Step.builder("Hop on a ferry")
+                        .addLane(Lane.builder()
+                                .addDirection(LaneDirection.create(
+                                        LaneDirection.SHAPE_NORMAL_LEFT, false))
+                                .build())
+                        .setLanesImage(CarIcon.ALERT)
+                        .build();
+        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+
+        RoutingInfo routingInfo =
+                RoutingInfo.builder()
+                        .setCurrentStep(currentStep, currentDistance)
+                        .setJunctionImage(CarIcon.ALERT)
+                        .setNextStep(currentStep)
+                        .build();
+
+        assertThat(routingInfo)
+                .isEqualTo(RoutingInfo.builder()
+                        .setCurrentStep(currentStep, currentDistance)
+                        .setJunctionImage(CarIcon.ALERT)
+                        .setNextStep(currentStep)
+                        .build());
+    }
+
+    @Test
+    public void notEquals_differentCurrentStep() {
+        Step currentStep =
+                Step.builder("Hop on a ferry")
+                        .addLane(Lane.builder()
+                                .addDirection(LaneDirection.create(
+                                        LaneDirection.SHAPE_NORMAL_LEFT, false))
+                                .build())
+                        .setLanesImage(CarIcon.APP_ICON)
+                        .build();
+        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+
+        RoutingInfo routingInfo =
+                RoutingInfo.builder().setCurrentStep(currentStep, currentDistance).build();
+
+        assertThat(routingInfo)
+                .isNotEqualTo(RoutingInfo.builder()
+                        .setCurrentStep(Step.builder("do a back flip")
+                                        .addLane(Lane.builder()
+                                                .addDirection(LaneDirection.create(
+                                                        LaneDirection.SHAPE_NORMAL_LEFT,
+                                                        false))
+                                                .build())
+                                        .setLanesImage(CarIcon.APP_ICON)
+                                        .build(),
+                                currentDistance)
+                        .build());
+    }
+
+    @Test
+    public void notEquals_differentCurrentDistance() {
+        Step currentStep = Step.builder("Hop on a ferry")
+                .addLane(Lane.builder()
+                        .addDirection(LaneDirection.create(
+                                LaneDirection.SHAPE_NORMAL_LEFT, false))
+                        .build())
+                .setLanesImage(CarIcon.APP_ICON)
+                .build();
+        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+
+        RoutingInfo routingInfo =
+                RoutingInfo.builder().setCurrentStep(currentStep, currentDistance).build();
+
+        assertThat(routingInfo)
+                .isNotEqualTo(RoutingInfo.builder()
+                        .setCurrentStep(currentStep,
+                                Distance.create(/* displayDistance= */ 200, Distance.UNIT_METERS))
+                        .build());
+    }
+
+    @Test
+    public void notEquals_differentJunctionImage() {
+        Step currentStep = Step.builder("Hop on a ferry")
+                .addLane(Lane.builder()
+                        .addDirection(LaneDirection.create(
+                                LaneDirection.SHAPE_NORMAL_LEFT, false))
+                        .build())
+                .setLanesImage(CarIcon.ALERT)
+                .build();
+        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+
+        RoutingInfo routingInfo = RoutingInfo.builder()
+                .setCurrentStep(currentStep, currentDistance)
+                .setJunctionImage(CarIcon.ALERT)
+                .setNextStep(currentStep)
+                .build();
+
+        assertThat(routingInfo)
+                .isNotEqualTo(RoutingInfo.builder()
+                        .setCurrentStep(currentStep, currentDistance)
+                        .setJunctionImage(CarIcon.ERROR)
+                        .setNextStep(currentStep)
+                        .build());
+    }
+
+    @Test
+    public void notEquals_differentNextStep() {
+        Step currentStep = Step.builder("Hop on a ferry")
+                .addLane(Lane.builder()
+                        .addDirection(LaneDirection.create(
+                                LaneDirection.SHAPE_NORMAL_LEFT, false))
+                        .build())
+                .setLanesImage(CarIcon.ALERT)
+                .build();
+        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+
+        RoutingInfo routingInfo = RoutingInfo.builder()
+                .setCurrentStep(currentStep, currentDistance)
+                .setNextStep(currentStep)
+                .build();
+
+        assertThat(routingInfo)
+                .isNotEqualTo(RoutingInfo.builder()
+                        .setCurrentStep(currentStep, currentDistance)
+                        .setNextStep(Step.builder("Do a backflip")
+                                .addLane(Lane.builder()
+                                        .addDirection(LaneDirection.create(
+                                                LaneDirection.SHAPE_NORMAL_LEFT, false))
+                                        .build())
+                                .setLanesImage(CarIcon.ALERT)
+                                .build())
+                        .build());
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/StepTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/StepTest.java
new file mode 100644
index 0000000..bc8a79d
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/StepTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static androidx.car.app.navigation.model.LaneDirection.SHAPE_SHARP_LEFT;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.model.CarIcon;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Step}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class StepTest {
+    @Test
+    public void createInstance() {
+        Lane lane = Lane.builder().addDirection(
+                LaneDirection.create(SHAPE_SHARP_LEFT, true)).build();
+        Maneuver maneuver =
+                Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW)
+                        .setRoundaboutExitNumber(/*roundaboutExitNumber=*/ 2)
+                        .setIcon(CarIcon.APP_ICON)
+                        .build();
+        String cue = "Left at State street.";
+        String road = "State St.";
+        Step step =
+                Step.builder(cue)
+                        .addLane(lane)
+                        .setLanesImage(CarIcon.APP_ICON)
+                        .setManeuver(maneuver)
+                        .setRoad(road)
+                        .build();
+
+        assertThat(step.getLanes()).hasSize(1);
+        assertThat(lane).isEqualTo(step.getLanes().get(0));
+        assertThat(CarIcon.APP_ICON).isEqualTo(step.getLanesImage());
+        assertThat(maneuver).isEqualTo(step.getManeuver());
+        assertThat(cue).isEqualTo(step.getCue().getText());
+        assertThat(road).isEqualTo(step.getRoad().getText());
+    }
+
+    @Test
+    public void clearLanes() {
+        Lane lane1 = Lane.builder().addDirection(
+                LaneDirection.create(SHAPE_SHARP_LEFT, true)).build();
+        Lane lane2 = Lane.builder()
+                .addDirection(LaneDirection.create(LaneDirection.SHAPE_SHARP_RIGHT, true))
+                .build();
+        String cue = "Left at State street.";
+        Step step = Step.builder(cue).addLane(lane1).addLane(lane2).clearLanes().build();
+
+        assertThat(step.getLanes()).hasSize(0);
+    }
+
+    @Test
+    public void createInstance_lanesImage_no_lanes_throws() {
+        String cue = "Left at State street.";
+
+        assertThrows(
+                IllegalStateException.class,
+                () -> Step.builder(cue).setLanesImage(CarIcon.APP_ICON).build());
+    }
+
+    @Test
+    public void equals() {
+        Lane lane = Lane.builder().addDirection(
+                LaneDirection.create(SHAPE_SHARP_LEFT, true)).build();
+        Maneuver maneuver = Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW)
+                .setRoundaboutExitNumber(/*roundaboutExitNumber=*/ 2)
+                .setIcon(CarIcon.APP_ICON)
+                .build();
+        String cue = "Left at State street.";
+        String road = "State St.";
+        Step step = Step.builder(cue)
+                .addLane(lane)
+                .setLanesImage(CarIcon.APP_ICON)
+                .setManeuver(maneuver)
+                .setRoad(road)
+                .build();
+
+        assertThat(Step.builder(cue)
+                .addLane(lane)
+                .setLanesImage(CarIcon.APP_ICON)
+                .setManeuver(maneuver)
+                .setRoad(road)
+                .build())
+                .isEqualTo(step);
+    }
+
+    @Test
+    public void notEquals_differentCue() {
+        String cue = "Left at State street.";
+        Step step = Step.builder(cue).build();
+
+        assertThat(Step.builder("foo").build()).isNotEqualTo(step);
+    }
+
+    @Test
+    public void notEquals_differentLane() {
+        Lane lane = Lane.builder().addDirection(
+                LaneDirection.create(SHAPE_SHARP_LEFT, true)).build();
+        String cue = "Left at State street.";
+
+        Step step = Step.builder(cue).addLane(lane).build();
+
+        assertThat(Step.builder(cue)
+                .addLane(Lane.builder()
+                        .addDirection(LaneDirection.create(SHAPE_SHARP_LEFT, false))
+                        .build())
+                .build())
+                .isNotEqualTo(step);
+    }
+
+    @Test
+    public void notEquals_differentLanesImage() {
+        String cue = "Left at State street.";
+        Lane lane = Lane.builder().addDirection(
+                LaneDirection.create(SHAPE_SHARP_LEFT, true)).build();
+
+        Step step = Step.builder(cue).addLane(lane).setLanesImage(CarIcon.APP_ICON).build();
+
+        assertThat(Step.builder(cue).addLane(lane).setLanesImage(CarIcon.ALERT).build())
+                .isNotEqualTo(step);
+    }
+
+    @Test
+    public void notEquals_differentManeuver() {
+        Maneuver maneuver =
+                Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW)
+                        .setRoundaboutExitNumber(/*roundaboutExitNumber=*/ 2)
+                        .setIcon(CarIcon.APP_ICON)
+                        .build();
+        String cue = "Left at State street.";
+
+        Step step = Step.builder(cue).setManeuver(maneuver).build();
+
+        assertThat(Step.builder(cue)
+                .setManeuver(Maneuver.builder(Maneuver.TYPE_DESTINATION).setIcon(
+                        CarIcon.APP_ICON).build())
+                .build())
+                .isNotEqualTo(step);
+    }
+
+    @Test
+    public void notEquals_differentRoad() {
+        String cue = "Left at State street.";
+
+        Step step = Step.builder(cue).setRoad("road").build();
+
+        assertThat(Step.builder(cue).setRoad("foo").build()).isNotEqualTo(step);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TravelEstimateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TravelEstimateTest.java
new file mode 100644
index 0000000..123b7ef
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TravelEstimateTest.java
@@ -0,0 +1,234 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static androidx.car.app.TestUtils.createDateTimeWithZone;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.DateTimeWithZone;
+import androidx.car.app.model.Distance;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for {@link TravelEstimate}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TravelEstimateTest {
+    private final DateTimeWithZone mArrivalTime =
+            createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
+    private final Distance mRemainingDistance =
+            Distance.create(/* displayDistance= */ 100, Distance.UNIT_METERS);
+    private final long mRemainingTime = TimeUnit.HOURS.toMillis(10);
+
+    // TODO(rampara): Investigate how to exercise minSDK requiring API
+//    @Test
+//    @Config(minSdk = VERSION_CODES.O)
+//    public void create_duration() {
+//        ZonedDateTime arrivalTime = ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]");
+//        Duration remainingTime = Duration.ofHours(10);
+//
+//        TravelEstimate travelEstimate =
+//                TravelEstimate.create(mRemainingDistance, remainingTime, arrivalTime);
+//
+//        assertThat(travelEstimate.getRemainingDistance()).isEqualTo(mRemainingDistance);
+//        assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(remainingTime.getSeconds
+//        ());
+//        assertDateTimeWithZoneEquals(arrivalTime, travelEstimate.getArrivalTimeAtDestination());
+//    }
+
+    @Test
+    public void create() {
+        DateTimeWithZone arrivalTime = createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
+        Distance remainingDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+        long remainingTime = TimeUnit.HOURS.toMillis(10);
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(remainingDistance,
+                        TimeUnit.MILLISECONDS.toSeconds(remainingTime),
+                        arrivalTime);
+
+        assertThat(travelEstimate.getRemainingDistance()).isEqualTo(remainingDistance);
+        assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(
+                TimeUnit.MILLISECONDS.toSeconds(remainingTime));
+        assertThat(travelEstimate.getArrivalTimeAtDestination()).isEqualTo(arrivalTime);
+        assertThat(travelEstimate.getRemainingTimeColor()).isEqualTo(CarColor.DEFAULT);
+    }
+
+    @Test
+    public void create_custom_remainingTimeColor() {
+        DateTimeWithZone arrivalTime = createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
+        Distance remainingDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+        long remainingTime = TimeUnit.HOURS.toMillis(10);
+
+        List<CarColor> allowedColors = new ArrayList<>();
+        allowedColors.add(CarColor.DEFAULT);
+        allowedColors.add(CarColor.PRIMARY);
+        allowedColors.add(CarColor.SECONDARY);
+        allowedColors.add(CarColor.RED);
+        allowedColors.add(CarColor.GREEN);
+        allowedColors.add(CarColor.BLUE);
+        allowedColors.add(CarColor.YELLOW);
+
+        for (CarColor carColor : allowedColors) {
+            TravelEstimate travelEstimate =
+                    TravelEstimate.builder(remainingDistance,
+                            TimeUnit.MILLISECONDS.toSeconds(remainingTime),
+                            arrivalTime)
+                            .setRemainingTimeColor(carColor)
+                            .build();
+
+            assertThat(travelEstimate.getRemainingDistance()).isEqualTo(remainingDistance);
+            assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(
+                    TimeUnit.MILLISECONDS.toSeconds(remainingTime));
+            assertThat(travelEstimate.getArrivalTimeAtDestination()).isEqualTo(arrivalTime);
+            assertThat(travelEstimate.getRemainingTimeColor()).isEqualTo(carColor);
+        }
+    }
+
+    @Test
+    public void create_custom_remainingDistanceColor() {
+        DateTimeWithZone arrivalTime = createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
+        Distance remainingDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+        long remainingTime = TimeUnit.HOURS.toMillis(10);
+
+        List<CarColor> allowedColors = new ArrayList<>();
+        allowedColors.add(CarColor.DEFAULT);
+        allowedColors.add(CarColor.PRIMARY);
+        allowedColors.add(CarColor.SECONDARY);
+        allowedColors.add(CarColor.RED);
+        allowedColors.add(CarColor.GREEN);
+        allowedColors.add(CarColor.BLUE);
+        allowedColors.add(CarColor.YELLOW);
+
+        for (CarColor carColor : allowedColors) {
+            TravelEstimate travelEstimate =
+                    TravelEstimate.builder(remainingDistance,
+                            TimeUnit.MILLISECONDS.toSeconds(remainingTime),
+                            arrivalTime)
+                            .setRemainingDistanceColor(carColor)
+                            .build();
+
+            assertThat(travelEstimate.getRemainingDistance()).isEqualTo(remainingDistance);
+            assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(
+                    TimeUnit.MILLISECONDS.toSeconds(remainingTime));
+            assertThat(travelEstimate.getArrivalTimeAtDestination()).isEqualTo(arrivalTime);
+            assertThat(travelEstimate.getRemainingDistanceColor()).isEqualTo(carColor);
+        }
+    }
+
+    @Test
+    public void create_custom_remainingTimeColor_invalid_throws() {
+        DateTimeWithZone arrivalTime = createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
+        Distance remainingDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+        long remainingTime = TimeUnit.HOURS.toMillis(10);
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        TravelEstimate.builder(remainingDistance,
+                                TimeUnit.MILLISECONDS.toSeconds(remainingTime),
+                                arrivalTime)
+                                .setRemainingTimeColor(CarColor.createCustom(1, 2)));
+    }
+
+    @Test
+    public void equals() {
+        TravelEstimate travelEstimate = TravelEstimate.create(mRemainingDistance,
+                TimeUnit.MILLISECONDS.toSeconds(mRemainingTime), mArrivalTime);
+
+        assertThat(travelEstimate)
+                .isEqualTo(
+                        TravelEstimate.create(mRemainingDistance,
+                                TimeUnit.MILLISECONDS.toSeconds(mRemainingTime),
+                                mArrivalTime));
+    }
+
+    @Test
+    public void notEquals_differentRemainingDistance() {
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(mRemainingDistance,
+                        TimeUnit.MILLISECONDS.toSeconds(mRemainingTime),
+                        mArrivalTime);
+
+        assertThat(travelEstimate)
+                .isNotEqualTo(
+                        TravelEstimate.create(
+                                Distance.create(/* displayDistance= */ 200, Distance.UNIT_METERS),
+                                TimeUnit.MILLISECONDS.toSeconds(mRemainingTime),
+                                mArrivalTime));
+    }
+
+    @Test
+    public void notEquals_differentRemainingTime() {
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(mRemainingDistance,
+                        TimeUnit.MILLISECONDS.toSeconds(mRemainingTime),
+                        mArrivalTime);
+
+        assertThat(travelEstimate)
+                .isNotEqualTo(
+                        TravelEstimate.create(mRemainingDistance,
+                                TimeUnit.MILLISECONDS.toSeconds(mRemainingTime) + 1,
+                                mArrivalTime));
+    }
+
+    @Test
+    public void notEquals_differentArrivalTime() {
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(mRemainingDistance,
+                        TimeUnit.MILLISECONDS.toSeconds(mRemainingTime),
+                        mArrivalTime);
+
+        assertThat(travelEstimate)
+                .isNotEqualTo(
+                        TravelEstimate.create(
+                                mRemainingDistance,
+                                TimeUnit.MILLISECONDS.toSeconds(mRemainingTime),
+                                createDateTimeWithZone("2020-04-14T15:57:01", "US/Pacific")));
+    }
+
+    @Test
+    public void notEquals_differentRemainingTimeColor() {
+        TravelEstimate travelEstimate =
+                TravelEstimate.builder(mRemainingDistance,
+                        TimeUnit.MILLISECONDS.toSeconds(mRemainingTime),
+                        mArrivalTime)
+                        .setRemainingTimeColor(CarColor.YELLOW)
+                        .build();
+
+        assertThat(travelEstimate)
+                .isNotEqualTo(
+                        TravelEstimate.builder(mRemainingDistance,
+                                TimeUnit.MILLISECONDS.toSeconds(mRemainingTime),
+                                mArrivalTime)
+                                .setRemainingTimeColor(CarColor.GREEN)
+                                .build());
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TripTest.java b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TripTest.java
new file mode 100644
index 0000000..4c936e3
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TripTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static androidx.car.app.TestUtils.createDateTimeWithZone;
+import static androidx.car.app.navigation.model.LaneDirection.SHAPE_SHARP_LEFT;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Distance;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeUnit;
+
+/** Tests for {@link Trip}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TripTest {
+
+    private final Step mStep =
+            Step.builder("Take the second exit of the roundabout.")
+                    .addLane(Lane.builder().addDirection(
+                            LaneDirection.create(SHAPE_SHARP_LEFT, true)).build())
+                    .setManeuver(Maneuver.builder(TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW)
+                            .setRoundaboutExitNumber(/*roundaboutExitNumber=*/ 2)
+                            .setIcon(CarIcon.APP_ICON)
+                            .build())
+                    .build();
+    private final Destination mDestination =
+            Destination.builder().setName("Google BVE").setAddress("1120 112th Ave NE").build();
+    private final TravelEstimate mStepTravelEstimate =
+            TravelEstimate.create(
+                    Distance.create(/* displayDistance= */ 10, Distance.UNIT_KILOMETERS),
+                    TimeUnit.HOURS.toSeconds(1),
+                    createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific"));
+    private final TravelEstimate mDestinationTravelEstimate =
+            TravelEstimate.create(
+                    Distance.create(/* displayDistance= */ 100, Distance.UNIT_KILOMETERS),
+                    TimeUnit.HOURS.toSeconds(1),
+                    createDateTimeWithZone("2020-04-14T16:57:00", "US/Pacific"));
+    private static final String ROAD = "State St.";
+
+    @Test
+    public void createInstance() {
+        Trip trip =
+                Trip.builder()
+                        .addDestination(mDestination)
+                        .addStep(mStep)
+                        .addDestinationTravelEstimate(mDestinationTravelEstimate)
+                        .addStepTravelEstimate(mStepTravelEstimate)
+                        .setCurrentRoad(ROAD)
+                        .setIsLoading(false)
+                        .build();
+
+        assertThat(trip.getDestinations()).hasSize(1);
+        assertThat(mDestination).isEqualTo(trip.getDestinations().get(0));
+        assertThat(trip.getSteps()).hasSize(1);
+        assertThat(mStep).isEqualTo(trip.getSteps().get(0));
+        assertThat(trip.getDestinationTravelEstimates()).hasSize(1);
+        assertThat(mDestinationTravelEstimate).isEqualTo(
+                trip.getDestinationTravelEstimates().get(0));
+        assertThat(trip.getStepTravelEstimates()).hasSize(1);
+        assertThat(mStepTravelEstimate).isEqualTo(trip.getStepTravelEstimates().get(0));
+        assertThat(trip.isLoading()).isFalse();
+    }
+
+    @Test
+    public void getDestinationWithEstimates_mismatch_count() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Trip.builder().addDestination(mDestination).build());
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Trip.builder().addDestinationTravelEstimate(
+                        mDestinationTravelEstimate).build());
+    }
+
+    @Test
+    public void getStepWithEstimates_mismatch_count() {
+        assertThrows(IllegalArgumentException.class, () -> Trip.builder().addStep(mStep).build());
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Trip.builder().addStepTravelEstimate(mStepTravelEstimate).build());
+    }
+
+    @Test
+    public void createInstance_loading_no_steps() {
+        Trip trip =
+                Trip.builder()
+                        .addDestination(mDestination)
+                        .addDestinationTravelEstimate(mDestinationTravelEstimate)
+                        .setCurrentRoad(ROAD)
+                        .setIsLoading(true)
+                        .build();
+
+        assertThat(trip.getDestinations()).hasSize(1);
+        assertThat(mDestination).isEqualTo(trip.getDestinations().get(0));
+        assertThat(trip.getSteps()).hasSize(0);
+        assertThat(trip.getDestinationTravelEstimates()).hasSize(1);
+        assertThat(mDestinationTravelEstimate).isEqualTo(
+                trip.getDestinationTravelEstimates().get(0));
+        assertThat(trip.getStepTravelEstimates()).hasSize(0);
+        assertThat(trip.isLoading()).isTrue();
+    }
+
+    @Test
+    public void createInstance_loading_with_steps() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Trip.builder().addStep(mStep).setIsLoading(true).build());
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Trip.builder().addStepTravelEstimate(mStepTravelEstimate).setIsLoading(
+                        true).build());
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> Trip.builder()
+                        .addStep(mStep)
+                        .addStepTravelEstimate(mStepTravelEstimate)
+                        .setIsLoading(true)
+                        .build());
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/notification/CarAppExtenderTest.java b/car/app/app/src/androidTest/java/androidx/car/app/notification/CarAppExtenderTest.java
new file mode 100644
index 0000000..72ef309
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/notification/CarAppExtenderTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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.car.app.notification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification.Action;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.test.R;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/** Tests for {@link CarAppExtender}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CarAppExtenderTest {
+    private static final String NOTIFICATION_CHANNEL_ID = "test carextender channel id";
+    private static final String INTENT_PRIMARY_ACTION =
+            "androidx.car.app.INTENT_PRIMARY_ACTION";
+    private static final String INTENT_SECONDARY_ACTION =
+            "androidx.car.app.INTENT_SECONDARY_ACTION";
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+
+    @Test
+    public void carAppExtender_checkDefaultValues() {
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                        .extend(
+                                // Simulate sending a notification that has the same bundle key
+                                // but no value is set.
+                                new NotificationCompat.Extender() {
+                                    @NonNull
+                                    @Override
+                                    public NotificationCompat.Builder extend(
+                                            @NonNull NotificationCompat.Builder builder) {
+                                        Bundle carExtensions = new Bundle();
+
+                                        builder.getExtras().putBundle("android.car.EXTENSIONS",
+                                                carExtensions);
+                                        return builder;
+                                    }
+                                });
+
+        CarAppExtender carAppExtender = new CarAppExtender(builder.build());
+        assertThat(carAppExtender.isExtended()).isFalse();
+        assertThat(carAppExtender.getContentTitle()).isNull();
+        assertThat(carAppExtender.getContentText()).isNull();
+        assertThat(carAppExtender.getSmallIconResId()).isEqualTo(0);
+        assertThat(carAppExtender.getLargeIconBitmap()).isNull();
+        assertThat(carAppExtender.getContentIntent()).isNull();
+        assertThat(carAppExtender.getDeleteIntent()).isNull();
+        assertThat(carAppExtender.getActions()).isEmpty();
+        assertThat(carAppExtender.getImportance())
+                .isEqualTo(NotificationManagerCompat.IMPORTANCE_UNSPECIFIED);
+    }
+
+    @Test
+    public void notification_extended() {
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                        .extend(CarAppExtender.builder().build());
+
+        assertThat(CarAppExtender.isExtended(builder.build())).isTrue();
+    }
+
+    @Test
+    public void notification_notExtended() {
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID);
+
+        assertThat(CarAppExtender.isExtended(builder.build())).isFalse();
+    }
+
+    @Test
+    public void notification_extended_setTitle() {
+        CharSequence title = "TestTitle";
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                        .extend(CarAppExtender.builder().setContentTitle(title).build());
+
+        assertThat(
+                title.toString().contentEquals(
+                        new CarAppExtender(builder.build()).getContentTitle()))
+                .isTrue();
+    }
+
+    @Test
+    public void notification_extended_setText() {
+        CharSequence text = "TestText";
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                        .extend(CarAppExtender.builder().setContentText(text).build());
+
+        assertThat(
+                text.toString().contentEquals(new CarAppExtender(builder.build()).getContentText()))
+                .isTrue();
+    }
+
+    @Test
+    public void notification_extended_setSmallIcon() {
+        int resId = R.drawable.ic_test_1;
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                        .extend(CarAppExtender.builder().setSmallIcon(resId).build());
+
+        assertThat(new CarAppExtender(builder.build()).getSmallIconResId()).isEqualTo(resId);
+    }
+
+    @Test
+    public void notification_extended_setLargeIcon() {
+        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_test_2);
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                        .extend(CarAppExtender.builder().setLargeIcon(bitmap).build());
+
+        assertThat(new CarAppExtender(builder.build()).getLargeIconBitmap()).isEqualTo(bitmap);
+    }
+
+    @Test
+    public void notification_extended_setContentIntent() {
+        Intent intent = new Intent(INTENT_PRIMARY_ACTION);
+        PendingIntent contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                        .extend(CarAppExtender.builder().setContentIntent(contentIntent).build());
+
+        assertThat(new CarAppExtender(builder.build()).getContentIntent()).isEqualTo(contentIntent);
+    }
+
+    @Test
+    public void notification_extended_setDeleteIntent() {
+        Intent intent = new Intent(INTENT_PRIMARY_ACTION);
+        PendingIntent deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                        .extend(CarAppExtender.builder().setDeleteIntent(deleteIntent).build());
+
+        assertThat(new CarAppExtender(builder.build()).getDeleteIntent()).isEqualTo(deleteIntent);
+    }
+
+    @Test
+    public void notification_extended_noActions() {
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                        .extend(CarAppExtender.builder().build());
+
+        assertThat(new CarAppExtender(builder.build()).getActions()).isEmpty();
+    }
+
+    @Test
+    public void notification_extended_addActions() {
+        int icon1 = R.drawable.ic_test_1;
+        CharSequence title1 = "FirstAction";
+        Intent intent1 = new Intent(INTENT_PRIMARY_ACTION);
+        PendingIntent actionIntent1 = PendingIntent.getBroadcast(mContext, 0, intent1, 0);
+
+        int icon2 = R.drawable.ic_test_2;
+        CharSequence title2 = "SecondAction";
+        Intent intent2 = new Intent(INTENT_SECONDARY_ACTION);
+        PendingIntent actionIntent2 = PendingIntent.getBroadcast(mContext, 0, intent2, 0);
+
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                        .extend(
+                                CarAppExtender.builder()
+                                        .addAction(icon1, title1, actionIntent1)
+                                        .addAction(icon2, title2, actionIntent2)
+                                        .build());
+
+        List<Action> actions = new CarAppExtender(builder.build()).getActions();
+        assertThat(actions).hasSize(2);
+        assertThat(actions.get(0).getIcon().getResId()).isEqualTo(icon1);
+        assertThat(title1.toString().contentEquals(actions.get(0).title)).isTrue();
+        assertThat(actions.get(0).actionIntent).isEqualTo(actionIntent1);
+        assertThat(actions.get(1).getIcon().getResId()).isEqualTo(icon2);
+        assertThat(title2.toString().contentEquals(actions.get(1).title)).isTrue();
+        assertThat(actions.get(1).actionIntent).isEqualTo(actionIntent2);
+    }
+
+    @Test
+    public void notification_extended_clearActions() {
+        int icon1 = R.drawable.ic_test_1;
+        CharSequence title1 = "FirstAction";
+        Intent intent1 = new Intent(INTENT_PRIMARY_ACTION);
+        PendingIntent actionIntent1 = PendingIntent.getBroadcast(mContext, 0, intent1, 0);
+
+        int icon2 = R.drawable.ic_test_2;
+        CharSequence title2 = "SecondAction";
+        Intent intent2 = new Intent(INTENT_SECONDARY_ACTION);
+        PendingIntent actionIntent2 = PendingIntent.getBroadcast(mContext, 0, intent2, 0);
+
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                        .extend(
+                                CarAppExtender.builder()
+                                        .addAction(icon1, title1, actionIntent1)
+                                        .addAction(icon2, title2, actionIntent2)
+                                        .clearActions()
+                                        .build());
+
+        List<Action> actions = new CarAppExtender(builder.build()).getActions();
+        assertThat(actions).isEmpty();
+    }
+
+    @Test
+    public void notification_extended_setImportance() {
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                        .extend(
+                                CarAppExtender.builder()
+                                        .setImportance(NotificationManagerCompat.IMPORTANCE_HIGH)
+                                        .build());
+
+        assertThat(new CarAppExtender(builder.build()).getImportance())
+                .isEqualTo(NotificationManagerCompat.IMPORTANCE_HIGH);
+    }
+}
diff --git a/car/app/app/src/androidTest/res/drawable/ic_test_1.xml b/car/app/app/src/androidTest/res/drawable/ic_test_1.xml
new file mode 100644
index 0000000..88398fc
--- /dev/null
+++ b/car/app/app/src/androidTest/res/drawable/ic_test_1.xml
@@ -0,0 +1,24 @@
+<!--
+ 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"/>
+</vector>
diff --git a/car/app/app/src/androidTest/res/drawable/ic_test_2.xml b/car/app/app/src/androidTest/res/drawable/ic_test_2.xml
new file mode 100644
index 0000000..cdcdf20c
--- /dev/null
+++ b/car/app/app/src/androidTest/res/drawable/ic_test_2.xml
@@ -0,0 +1,24 @@
+<!--
+ 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="64dp"
+    android:height="64dp"
+    android:viewportWidth="64.0"
+    android:viewportHeight="64.0">
+  <path
+      android:fillColor="#FF000000"/>
+</vector>
diff --git a/car/app/app/src/main/AndroidManifest.xml b/car/app/app/src/main/AndroidManifest.xml
index a9ecfe8..188169a 100644
--- a/car/app/app/src/main/AndroidManifest.xml
+++ b/car/app/app/src/main/AndroidManifest.xml
@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
+
 <!--
   Copyright 2020 The Android Open Source Project
 
@@ -14,7 +15,6 @@
   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.car.app">
+<manifest package="androidx.car.app">
 
 </manifest>
\ No newline at end of file
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl
index 3639a6f..8383e12 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl
@@ -18,9 +18,7 @@
 
 import androidx.car.app.IOnDoneCallback;
 
-/**
- * @hide
- */
+/** @hide */
 oneway interface IOnCheckedChangeListener {
   void onCheckedChange(boolean isChecked, IOnDoneCallback callback) = 1;
 }
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl
index 8ca1db6..ff43f3f 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl
@@ -18,9 +18,7 @@
 
 import androidx.car.app.IOnDoneCallback;
 
-/**
- * @hide
- */
+/** @hide */
 oneway interface IOnItemVisibilityChangedListener {
   /**
    * A callback for when the range of items that are visible in the UI changes.
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
index a483c01..2896a83 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
@@ -18,9 +18,7 @@
 
 import androidx.car.app.IOnDoneCallback;
 
-/**
- * @hide
- */
+/** @hide */
 oneway interface IOnSelectedListener {
   void onSelected(int index, IOnDoneCallback callback) = 1;
 }
diff --git a/car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl
index 7bf092b..9af81e0 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl
@@ -18,9 +18,7 @@
 
 import androidx.car.app.IOnDoneCallback;
 
-/**
- * @hide
- */
+/** @hide */
 oneway interface ISearchListener {
   void onSearchTextChanged(String text, IOnDoneCallback callback) = 1;
   void onSearchSubmitted(String text, IOnDoneCallback callback) = 2;
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IStartCarApp.aidl b/car/app/app/src/main/aidl/androidx/car/app/IStartCarApp.aidl
index c686a13..14ef334 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IStartCarApp.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/IStartCarApp.aidl
@@ -18,9 +18,7 @@
 
 import androidx.car.app.IOnDoneCallback;
 
-/**
- * @hide
- */
+/** @hide */
 interface IStartCarApp {
    /**
    * Starts the car app on the car screen.
diff --git a/car/app/app/src/main/aidl/androidx/car/app/ISurfaceListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/ISurfaceListener.aidl
index 87482ed..7d9192d 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/ISurfaceListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/ISurfaceListener.aidl
@@ -22,9 +22,7 @@
 import androidx.car.app.serialization.Bundleable;
 import androidx.car.app.IOnDoneCallback;
 
-/**
- * @hide
- */
+/** @hide */
 oneway interface ISurfaceListener {
   /**
    * Notifies the app that the surface has changed.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java b/car/app/app/src/main/aidl/androidx/car/app/model/IOnClickListener.aidl
similarity index 71%
copy from camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java
copy to car/app/app/src/main/aidl/androidx/car/app/model/IOnClickListener.aidl
index d6b6436..7308d03 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java
+++ b/car/app/app/src/main/aidl/androidx/car/app/model/IOnClickListener.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-/**
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-package androidx.camera.core.impl.quirk;
+package androidx.car.app.model;
 
-import androidx.annotation.RestrictTo;
+import androidx.car.app.IOnDoneCallback;
+
+/** @hide */
+oneway interface IOnClickListener {
+  void onClick(IOnDoneCallback callback) = 1;
+}
diff --git a/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationHost.aidl b/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationHost.aidl
new file mode 100644
index 0000000..f86e45d
--- /dev/null
+++ b/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationHost.aidl
@@ -0,0 +1,38 @@
+/*
+ * 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.car.app.navigation;
+
+import androidx.car.app.serialization.Bundleable;
+
+/** @hide */
+interface INavigationHost {
+ /**
+  * Update the host when active navigation in the app has started.
+  */
+  void navigationStarted() = 1;
+
+ /**
+  * Update the host when active navigation in the app has ended.
+  */
+  void navigationEnded() = 2;
+
+  /**
+   * Sends the navigation state to the host which can be rendered at different
+   * places in the car such as the navigation templates, cluster screens, etc.
+   */
+  void updateTrip(in Bundleable trip) = 3;
+}
diff --git a/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl b/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl
new file mode 100644
index 0000000..4e16e7e
--- /dev/null
+++ b/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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.car.app.navigation;
+
+import androidx.car.app.IOnDoneCallback;
+
+/**
+ * @hide
+ */
+oneway interface INavigationManager {
+ /**
+  * Notifies the app that it should stop the active navigation right away.
+  *
+  * <p>The app should stop any audio guidance, routing notifications tagged for
+  * the car, and metadata state updates.
+  */
+  void stopNavigation(IOnDoneCallback callback) = 1;
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/AppManager.java b/car/app/app/src/main/java/androidx/car/app/AppManager.java
index fca68a0..131ab08 100644
--- a/car/app/app/src/main/java/androidx/car/app/AppManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/AppManager.java
@@ -24,19 +24,15 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.car.app.model.TemplateWrapper;
 import androidx.car.app.utils.RemoteUtils;
 import androidx.car.app.utils.ThreadUtils;
 
 import java.util.Objects;
 
-// TODO(rampara): Uncomment on commit of model modules.
-//import androidx.car.app.model.TemplateWrapper;
-
 /** Manages the communication between the app and the host. */
 public class AppManager {
     @NonNull
-    @SuppressWarnings("UnusedVariable")
-    // TODO(rampara): Remove suppress annotation on commit of model modules.
     private final CarContext mCarContext;
     @NonNull
     private final IAppManager.Stub mAppManager;
@@ -68,9 +64,8 @@
                 "setSurfaceListener");
     }
 
-    // TODO(rampara): Change code tags to link after commit of model module.
     /**
-     * Requests the current template to be invalidated, which eventually triggers a call to {@code
+     * Requests the current template to be invalidated, which eventually triggers a call to {@link
      * Screen#getTemplate} to get the new template to display.
      *
      * @throws HostException if the remote call fails.
@@ -127,22 +122,21 @@
                     public void getTemplate(IOnDoneCallback callback) {
                         ThreadUtils.runOnMain(
                                 () -> {
-                                    // TODO(rampara): Uncomment on commit of model modules.
-//                                    TemplateWrapper templateWrapper;
-//                                    try {
-//                                        templateWrapper =
-//                                                AppManager.this
-//                                                        .mCarContext
-//                                                        .getCarService(ScreenManager.class)
-//                                                        .getTopTemplate();
-//                                    } catch (RuntimeException e) {
-//                                        RemoteUtils.sendFailureResponse(callback,
-//                                        "getTemplate", e);
-//                                        throw new WrappedRuntimeException(e);
-//                                    }
-//
-//                                    RemoteUtils.sendSuccessResponse(callback, "getTemplate",
-//                                            templateWrapper);
+                                    TemplateWrapper templateWrapper;
+                                    try {
+                                        templateWrapper =
+                                                AppManager.this
+                                                        .mCarContext
+                                                        .getCarService(ScreenManager.class)
+                                                        .getTopTemplate();
+                                    } catch (RuntimeException e) {
+                                        RemoteUtils.sendFailureResponse(callback,
+                                                "getTemplate", e);
+                                        throw new WrappedRuntimeException(e);
+                                    }
+
+                                    RemoteUtils.sendSuccessResponse(callback, "getTemplate",
+                                            templateWrapper);
                                 });
                     }
 
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppService.java b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
index a322d00..58a108f 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppService.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
@@ -30,6 +30,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.car.app.CarContext.CarServiceType;
+import androidx.car.app.navigation.NavigationManager;
 import androidx.car.app.serialization.Bundleable;
 import androidx.car.app.serialization.BundlerException;
 import androidx.car.app.utils.RemoteUtils;
@@ -44,9 +45,6 @@
 import java.io.PrintWriter;
 import java.security.InvalidParameterException;
 
-// TODO(rampara): Uncomment on addition of navigation module
-//import androidx.car.app.navigation.NavigationManager;
-
 /**
  * The base class for implementing a car app that runs in the car.
  *
@@ -107,8 +105,7 @@
             mRegistry.handleLifecycleEvent(Event.ON_STOP);
 
             // Stop any active navigation
-            // TODO(rampara): Uncomment on addition of navigation module
-//          carContext.getCarService(NavigationManager.class).stopNavigation();
+            mCarContext.getCarService(NavigationManager.class).stopNavigation();
 
             // Destroy all screens in the stack
             mCarContext.getCarService(ScreenManager.class).destroyAndClearScreenStack();
@@ -192,13 +189,12 @@
         return mCarContext;
     }
 
-    // TODO(rampara): Replace code tags with links on addition of model module.
     /**
      * Requests the first {@link Screen} for the application.
      *
      * <p>This method is invoked when this car app is first opened by the user.
      *
-     * <p>Once the method returns, {@code Screen#getTemplate} will be called on the {@link Screen}
+     * <p>Once the method returns, {@link Screen#getTemplate} will be called on the {@link Screen}
      * returned, and the app will be displayed on the car screen.
      *
      * <p>To pre-seed a back stack, you can push {@link Screen}s onto the stack, via {@link
@@ -218,11 +214,10 @@
     @NonNull
     public abstract Screen onCreateScreen(@NonNull Intent intent);
 
-    // TODO(rampara): Replace code tags with links on addition of model module.
     /**
      * Notifies that the car app has received a new {@link Intent}.
      *
-     * <p>Once the method returns, {@code Screen#getTemplate} will be called on the {@link Screen}
+     * <p>Once the method returns, {@link Screen#getTemplate} will be called on the {@link Screen}
      * that is on top of the {@link Screen} stack managed by the {@link ScreenManager}, and the app
      * will be displayed on the car screen.
      *
@@ -255,7 +250,6 @@
     public void onCarConfigurationChanged(@NonNull Configuration newConfiguration) {
     }
 
-    // TODO(rampara): Replace code tags with links on addition of model module.
     /**
      * Returns the {@link CarAppService}'s {@link Lifecycle}.
      *
@@ -264,8 +258,7 @@
      * <ul>
      *   <li>Observe its {@link Lifecycle} by calling {@link Lifecycle#addObserver}. You can use the
      *       {@link androidx.lifecycle.LifecycleObserver} to take specific actions whenever the
-     *       {@link
-     *       Screen} receives different {@link Event}s.
+     *       {@link Screen} receives different {@link Event}s.
      *   <li>Use this {@link CarAppService} to observe {@link androidx.lifecycle.LiveData}s that
      *       may drive the backing data for your application.
      * </ul>
@@ -293,7 +286,7 @@
      *       app is no longer displaying in the car, the host may finish this car app.
      *   <dt>{@link Event#ON_DESTROY}
      *   <dd>The OS has now destroyed this {@link CarAppService} instance, and it is no longer
-     *   valid.
+     *       valid.
      * </dl>
      *
      * <p>Listeners that are added in {@link Event#ON_START}, should be removed in {@link
@@ -318,8 +311,7 @@
 
         for (String arg : args) {
             if (AUTO_DRIVE.equals(arg)) {
-                // TODO(rampara): Uncomment on addition of navigation module
-//        runOnMain(carContext.getCarService(NavigationManager.class)::onAutoDriveEnabled);
+                runOnMain(mCarContext.getCarService(NavigationManager.class)::onAutoDriveEnabled);
             }
         }
     }
@@ -435,11 +427,11 @@
                                             AppManager.class).getIInterface());
                             return;
                         case CarContext.NAVIGATION_SERVICE:
-                            // TODO(rampara): Uncomment on addition of navigation module
-//                RemoteUtils.sendSuccessResponse(
-//                  callback,
-//                  "getManager",
-//                  carContext.getCarService(NavigationManager.class).getIInterface());
+                            RemoteUtils.sendSuccessResponse(
+                                    callback,
+                                    "getManager",
+                                    mCarContext.getCarService(
+                                            NavigationManager.class).getIInterface());
                             return;
                         default:
                             Log.e(TAG, type + "%s is not a valid manager");
diff --git a/car/app/app/src/main/java/androidx/car/app/CarContext.java b/car/app/app/src/main/java/androidx/car/app/CarContext.java
index 826614d..8c44fe5 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarContext.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarContext.java
@@ -40,6 +40,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.StringDef;
+import androidx.car.app.navigation.NavigationManager;
 import androidx.car.app.utils.RemoteUtils;
 import androidx.car.app.utils.ThreadUtils;
 import androidx.lifecycle.Lifecycle;
@@ -49,16 +50,12 @@
 import java.lang.annotation.RetentionPolicy;
 import java.security.InvalidParameterException;
 
-// TODO(rampara): Uncomment on addition of navigation module
-//import androidx.car.app.navigation.NavigationManager;
-
-// TODO(rampara): Replace code tags with links on addition of model module.
 /**
  * The CarContext class is a {@link ContextWrapper} subclass accessible to your {@link
  * CarAppService} and {@link Screen} instances, which provides access to car services such as the
  * {@link ScreenManager} for managing the screen stack, the {@link AppManager} for general
  * app-related functionality such as accessing a surface for drawing your navigation app’s map, and
- * the {@code NavigationManager} used by turn-by-turn navigation apps to communicate navigation
+ * the {@link NavigationManager} used by turn-by-turn navigation apps to communicate navigation
  * metadata and other navigation-related events with the host. See Access the navigation templates
  * for a comprehensive list of library functionality available to navigation apps.
  *
@@ -121,8 +118,7 @@
             "androidx.car.app.action.NAVIGATE";
 
     private final AppManager mAppManager;
-    // TODO(rampara): Uncomment on addition of navigation module
-    //  private final NavigationManager navigationManager;
+    private final NavigationManager mNavigationManager;
     private final ScreenManager mScreenManager;
     private final OnBackPressedDispatcher mOnBackPressedDispatcher;
 
@@ -135,7 +131,6 @@
         return new CarContext(lifecycle, new HostDispatcher());
     }
 
-    // TODO(rampara): Replace code tags with links on addition of model module.
     /**
      * Provides a car service by name.
      *
@@ -147,7 +142,7 @@
      *   <dt>{@link #APP_SERVICE}
      *   <dd>An {@link AppManager} for communication between the app and the host.
      *   <dt>{@link #NAVIGATION_SERVICE}
-     *   <dd>A {@code NavigationManager} for management of navigation updates.
+     *   <dd>A {@link NavigationManager} for management of navigation updates.
      *   <dt>{@link #SCREEN_MANAGER_SERVICE}
      *   <dd>A {@link ScreenManager} for management of {@link Screen}s.
      * </dl>
@@ -165,9 +160,8 @@
         switch (requireNonNull(name)) {
             case APP_SERVICE:
                 return mAppManager;
-            // TODO(rampara): Uncomment on addition of navigation module
-//      case NAVIGATION_SERVICE:
-//        return navigationManager;
+            case NAVIGATION_SERVICE:
+                return mNavigationManager;
             case SCREEN_MANAGER_SERVICE:
                 return mScreenManager;
             default: // fall out
@@ -177,11 +171,10 @@
                 "The name '" + name + "' does not correspond to a car service.");
     }
 
-    // TODO(rampara): Replace code tags with links on addition of model module.
     /**
      * Returns the a car service, by class.
      *
-     * <p>Currently supported classes are: {@link AppManager}, {@code NavigationManager}, {@link
+     * <p>Currently supported classes are: {@link AppManager}, {@link NavigationManager}, {@link
      * ScreenManager}.
      *
      * @param serviceClass the class of the requested service.
@@ -210,9 +203,8 @@
     public String getCarServiceName(@NonNull Class<?> serviceClass) {
         if (requireNonNull(serviceClass).isInstance(mAppManager)) {
             return APP_SERVICE;
-            // TODO(rampara): Uncomment on addition of navigation module
-//    } else if (serviceClass.isInstance(navigationManager)) {
-//      return NAVIGATION_SERVICE;
+        } else if (serviceClass.isInstance(mNavigationManager)) {
+            return NAVIGATION_SERVICE;
         } else if (serviceClass.isInstance(mScreenManager)) {
             return SCREEN_MANAGER_SERVICE;
         }
@@ -450,8 +442,7 @@
 
         this.mHostDispatcher = hostDispatcher;
         mAppManager = AppManager.create(this, hostDispatcher);
-        // TODO(rampara): Uncomment on addition of navigation module
-        //navigationManager = NavigationManager.create(hostDispatcher);
+        mNavigationManager = NavigationManager.create(hostDispatcher);
         mScreenManager = ScreenManager.create(this, lifecycle);
         mOnBackPressedDispatcher =
                 new OnBackPressedDispatcher(() -> getCarService(ScreenManager.class).pop());
diff --git a/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java b/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
index dc17bc4..3453d3b 100644
--- a/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
+++ b/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
@@ -29,8 +29,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.car.app.CarContext.CarServiceType;
-// TODO(rampara): Uncomment on addition of navigation module
-//import androidx.car.app.navigation.INavigationHost;
+import androidx.car.app.navigation.INavigationHost;
 import androidx.car.app.utils.RemoteUtils;
 import androidx.car.app.utils.ThreadUtils;
 
@@ -47,8 +46,8 @@
     private ICarHost mCarHost;
     @Nullable
     private IAppHost mAppHost;
-    // TODO(rampara): Uncomment on addition of navigation module
-//  @Nullable private INavigationHost navigationHost;
+    @Nullable
+    private INavigationHost mNavigationHost;
 
     /**
      * Dispatches the {@code call} to the host for the given {@code hostType}.
@@ -85,8 +84,7 @@
 
         mCarHost = null;
         mAppHost = null;
-        // TODO(rampara): Uncomment on addition of navigation module
-//    navigationHost = null;
+        mNavigationHost = null;
     }
 
     /**
@@ -114,17 +112,17 @@
                 host = mAppHost;
                 break;
             case CarContext.NAVIGATION_SERVICE:
-                // TODO(rampara): Uncomment on addition of navigation module
-//        if (navigationHost == null) {
-//          navigationHost =
-//              RemoteUtils.call(
-//                  () ->
-//                      INavigationHost.Stub.asInterface(
-//                          requireNonNull(carHost).getHost(CarContext.NAVIGATION_SERVICE)),
-//                  "getHost(Navigation)");
-//        }
-//        host = navigationHost;
-//        break;
+                if (mNavigationHost == null) {
+                    mNavigationHost =
+                            RemoteUtils.call(
+                                    () ->
+                                            INavigationHost.Stub.asInterface(
+                                                    requireNonNull(mCarHost).getHost(
+                                                            CarContext.NAVIGATION_SERVICE)),
+                                    "getHost(Navigation)");
+                }
+                host = mNavigationHost;
+                break;
             case CarContext.CAR_SERVICE:
                 host = mCarHost;
                 break;
diff --git a/car/app/app/src/main/java/androidx/car/app/Screen.java b/car/app/app/src/main/java/androidx/car/app/Screen.java
index b7be413..d78d413 100644
--- a/car/app/app/src/main/java/androidx/car/app/Screen.java
+++ b/car/app/app/src/main/java/androidx/car/app/Screen.java
@@ -17,12 +17,18 @@
 package androidx.car.app;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.car.app.utils.CommonUtils.TAG;
 
 import static java.util.Objects.requireNonNull;
 
+import android.util.Log;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateInfo;
+import androidx.car.app.model.TemplateWrapper;
 import androidx.car.app.utils.ThreadUtils;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.Lifecycle.Event;
@@ -30,16 +36,10 @@
 import androidx.lifecycle.LifecycleOwner;
 import androidx.lifecycle.LifecycleRegistry;
 
-// TODO(rampara): Uncomment on addition of model module
-//import androidx.car.app.model.Template;
-//import androidx.car.app.model.TemplateInfo;
-//import androidx.car.app.model.TemplateWrapper;
-
-// TODO(rampara): Replace code tags with links on addition of model module.
 /**
- * A Screen has a {@link Lifecycle} and provides the mechanism for the app to send {@code Template}s
+ * A Screen has a {@link Lifecycle} and provides the mechanism for the app to send {@link Template}s
  * to display when the Screen is visible. Screen instances can also be pushed and popped to and from
- * a Screen stack, which ensures they adhere to the template flow restrictions (see {@code
+ * a Screen stack, which ensures they adhere to the template flow restrictions (see {@link
  * #getTemplate} for more details on template flow).
  *
  * <p>The Screen class can be used to manage individual units of business logic within a car app. A
@@ -77,39 +77,32 @@
      * A reference to the last template returned by this screen, or {@code null} if one has not been
      * returned yet.
      */
-    // TODO(rampara): Uncomment on addition of model module
-//    @Nullable
-//    private TemplateWrapper mTemplateWrapper;
+    @Nullable
+    private TemplateWrapper mTemplateWrapper;
 
-    // TODO(rampara): Uncomment on addition of model module
     /**
      * Whether to set the ID of the last template in the next template to be returned.
      *
-//     * @see #getTemplate
+     * @see #getTemplate
      */
-    @SuppressWarnings("UnusedVariable")
-    // TODO(rampara): Remove suppress annotation on commit of model modules.
     private boolean mUseLastTemplateId;
 
     protected Screen(@NonNull CarContext carContext) {
         this.mCarContext = requireNonNull(carContext);
     }
 
-    // TODO(rampara): Replace code tags with links on addition of model module.
     /**
-     * Requests the current template to be invalidated, which eventually triggers a call to {@code
+     * Requests the current template to be invalidated, which eventually triggers a call to {@link
      * #getTemplate} to get the new template to display.
      *
      * <p>If the current {@link State} of this screen is not at least {@link State#STARTED}, then a
      * call to this method will have no effect.
      *
      * <p>After the call to invalidate is made, subsequent calls have no effect until the new
-     * template
-     * is returned by {@code #getTemplate}.
+     * template is returned by {@link #getTemplate}.
      *
-     * <p>To avoid race conditions with calls to {@code #getTemplate} you should call this method
-     * with
-     * the main thread.
+     * <p>To avoid race conditions with calls to {@link #getTemplate} you should call this method
+     * with the main thread.
      *
      * @throws HostException if the remote call fails.
      */
@@ -170,7 +163,6 @@
         return mMarker;
     }
 
-    // TODO(rampara): Replace code tags with links on addition of model module.
     /**
      * Returns this screen's lifecycle.
      *
@@ -191,7 +183,7 @@
      *   <dt>{@link Event#ON_CREATE}
      *   <dd>The screen is in the process of being pushed to the screen stack, it is valid, but
      *       contents from it are not yet visible in the car screen. You should get a callback to
-     *       {@code #getTemplate} at a point after this call.
+     *       {@link #getTemplate} at a point after this call.
      *   <dt>{@link Event#ON_START}
      *   <dd>The template returned from this screen is visible in the car screen.
      *   <dt>{@link Event#ON_RESUME}
@@ -230,6 +222,7 @@
         return mCarContext.getCarService(ScreenManager.class);
     }
 
+    // TODO(rampara): Replace code tags with link on submission of notification module
     /**
      * Returns the {@link Template} to present in the car screen.
      *
@@ -307,16 +300,15 @@
      * an app to begin a new task flow from notifications, and it holds true even if an app is
      * already bound and in the foreground.
      *
-     * <p>See {@link androidx.car.app.notification.CarAppExtender} for details on notifications.
+     * <p>See {@code androidx.car.app.notification.CarAppExtender} for details on notifications.
      */
-    // TODO(rampara): Uncomment on addition of model module
-//    @NonNull
-//    public abstract Template getTemplate();
-//
-//    /** Sets a {@link OnScreenResultCallback} for this {@link Screen}. */
-//    void setOnResultCallback(OnScreenResultCallback onScreenResultCallback) {
-//        this.mOnScreenResultCallback = onScreenResultCallback;
-//    }
+    @NonNull
+    public abstract Template getTemplate();
+
+    /** Sets a {@link OnScreenResultCallback} for this {@link Screen}. */
+    void setOnResultCallback(OnScreenResultCallback onScreenResultCallback) {
+        this.mOnScreenResultCallback = onScreenResultCallback;
+    }
 
     /**
      * Dispatches lifecycle event for {@code event} on the main thread.
@@ -350,27 +342,26 @@
      * that is stamped with the same ID as the last template returned by this screen. This is
      * used to identify back (stack pop) operations.
      */
-    // TODO(rampara): Uncomment on addition of model module
-//    @NonNull
-//    TemplateWrapper getTemplateWrapper() {
-//        Template template = getTemplate();
-//
-//        TemplateWrapper wrapper;
-//        if (mUseLastTemplateId) {
-//            wrapper =
-//                    TemplateWrapper.wrap(
-//                            template, getLastTemplateInfo(
-//                                    requireNonNull(mTemplateWrapper)).getTemplateId());
-//        } else {
-//            wrapper = TemplateWrapper.wrap(template);
-//        }
-//        mUseLastTemplateId = false;
-//
-//        mTemplateWrapper = wrapper;
-//
-//        Log.d(TAG, "Returning " + template + " from screen " + this);
-//        return wrapper;
-//    }
+    @NonNull
+    TemplateWrapper getTemplateWrapper() {
+        Template template = getTemplate();
+
+        TemplateWrapper wrapper;
+        if (mUseLastTemplateId) {
+            wrapper =
+                    TemplateWrapper.wrap(
+                            template, getLastTemplateInfo(
+                                    requireNonNull(mTemplateWrapper)).getTemplateId());
+        } else {
+            wrapper = TemplateWrapper.wrap(template);
+        }
+        mUseLastTemplateId = false;
+
+        mTemplateWrapper = wrapper;
+
+        Log.d(TAG, "Returning " + template + " from screen " + this);
+        return wrapper;
+    }
 
     /**
      * Returns the information for the template that was last returned by this screen.
@@ -381,24 +372,22 @@
      * dispatched to the top screen, allowing to notify the host of the current stack of template
      * ids known to the client.
      */
-    // TODO(rampara): Uncomment on addition of model module
-//    @NonNull
-//    TemplateInfo getLastTemplateInfo() {
-//        if (mTemplateWrapper == null) {
-//            mTemplateWrapper = TemplateWrapper.wrap(getTemplate());
-//        }
-//        return new TemplateInfo(mTemplateWrapper.getTemplate(), mTemplateWrapper.getId());
-//    }
-//
-//    @NonNull
-//    private static TemplateInfo getLastTemplateInfo(TemplateWrapper lastTemplateWrapper) {
-//        return new TemplateInfo(lastTemplateWrapper.getTemplate(), lastTemplateWrapper.getId());
-//    }
+    @NonNull
+    TemplateInfo getLastTemplateInfo() {
+        if (mTemplateWrapper == null) {
+            mTemplateWrapper = TemplateWrapper.wrap(getTemplate());
+        }
+        return new TemplateInfo(mTemplateWrapper.getTemplate(), mTemplateWrapper.getId());
+    }
 
-    // TODO(rampara): Replace code tags with links on addition of model module.
+    @NonNull
+    private static TemplateInfo getLastTemplateInfo(TemplateWrapper lastTemplateWrapper) {
+        return new TemplateInfo(lastTemplateWrapper.getTemplate(), lastTemplateWrapper.getId());
+    }
+
     /**
-     * Denotes whether the next {@code Template} retrieved via {@code #getTemplate} should reuse the
-     * ID of the last {@code Template}.
+     * Denotes whether the next {@link Template} retrieved via {@link #getTemplate} should reuse the
+     * ID of the last {@link Template}.
      *
      * <p>When this is set to {@code true}, the host will considered the next template sent to be a
      * back operation, and will attempt to find the previous template that shares the same ID and
diff --git a/car/app/app/src/main/java/androidx/car/app/ScreenManager.java b/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
index 3cd433f..56b3365 100644
--- a/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
@@ -27,9 +27,8 @@
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-// TODO(rampara): Uncomment on commit of model modules.
-//import androidx.car.app.model.TemplateInfo;
-//import androidx.car.app.model.TemplateWrapper;
+import androidx.car.app.model.TemplateInfo;
+import androidx.car.app.model.TemplateWrapper;
 import androidx.car.app.utils.ThreadUtils;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.Lifecycle;
@@ -92,8 +91,7 @@
     @SuppressLint("ExecutorRegistration")
     public void pushForResult(
             @NonNull Screen screen, @NonNull OnScreenResultCallback onScreenResultCallback) {
-        // TODO(rampara): Uncomment on addition of model module
-//        requireNonNull(screen).setOnResultCallback(requireNonNull(onScreenResultCallback));
+        requireNonNull(screen).setOnResultCallback(requireNonNull(onScreenResultCallback));
         pushInternal(screen);
     }
 
@@ -170,26 +168,24 @@
     }
 
     /** Returns the {@link TemplateWrapper} for the {@link Screen} that is on top of the stack. */
-//    @NonNull
-//    @MainThread
-    // TODO(rampara): Uncomment on commit of model modules.
-//    TemplateWrapper getTopTemplate() {
-//        ThreadUtils.checkMainThread();
-//
-//        Screen screen = getTop();
-//        Log.d(TAG, "Requesting template from Screen " + screen);
-//
-//        TemplateWrapper templateWrapper = screen.getTemplateWrapper();
-//
-//        List<TemplateInfo> templateInfoList = new ArrayList<>();
-//        for (Screen s : mScreenStack) {
-//            templateInfoList.add(s.getLastTemplateInfo());
-//        }
-//
-//        templateWrapper.setTemplateInfosForScreenStack(templateInfoList);
-//        return templateWrapper;
-//        return templateWrapper;
-//    }
+    @NonNull
+    @MainThread
+    TemplateWrapper getTopTemplate() {
+        ThreadUtils.checkMainThread();
+
+        Screen screen = getTop();
+        Log.d(TAG, "Requesting template from Screen " + screen);
+
+        TemplateWrapper templateWrapper = screen.getTemplateWrapper();
+
+        List<TemplateInfo> templateInfoList = new ArrayList<>();
+        for (Screen s : mScreenStack) {
+            templateInfoList.add(s.getLastTemplateInfo());
+        }
+
+        templateWrapper.setTemplateInfosForScreenStack(templateInfoList);
+        return templateWrapper;
+    }
 
     void destroyAndClearScreenStack() {
         for (Screen screen : mScreenStack) {
diff --git a/car/app/app/src/main/java/androidx/car/app/WrappedRuntimeException.java b/car/app/app/src/main/java/androidx/car/app/WrappedRuntimeException.java
index cf38d0b..0a7307a 100644
--- a/car/app/app/src/main/java/androidx/car/app/WrappedRuntimeException.java
+++ b/car/app/app/src/main/java/androidx/car/app/WrappedRuntimeException.java
@@ -22,7 +22,6 @@
  * A wrapper to allow rethrowing any {@link RuntimeException} that the car app threw, after
  * notifying the host of them.
  */
-// Developers can catch this exception so keeping it for stack traces.
 public class WrappedRuntimeException extends RuntimeException {
     public WrappedRuntimeException(@Nullable Throwable cause) {
         super(cause);
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Action.java b/car/app/app/src/main/java/androidx/car/app/model/Action.java
new file mode 100644
index 0000000..0d21904
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Action.java
@@ -0,0 +1,417 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.model.CarColor.DEFAULT;
+import static androidx.car.app.model.constraints.CarColorConstraints.STANDARD_ONLY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+import android.text.TextUtils;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.activity.OnBackPressedDispatcher;
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.CarContext;
+import androidx.car.app.model.constraints.CarIconConstraints;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represents an action with an optional icon and text.
+ *
+ * <p>Actions may be displayed differently depending on the template or model they are added to. For
+ * example, the host may decide to display an action as a floating action button (FAB) when
+ * displayed over a map, as a button when displayed in a {@link Pane}, or as a simple icon with no
+ * title when displayed within a {@link Row}.
+ *
+ * <h4>Standard actions</h4>
+ *
+ * A set of standard, built-in {@link Action} instances is available with a few of the common basic
+ * actions car apps may need (for example a {@link #BACK} action).
+ *
+ * <p>With the exception of {@link #APP_ICON} and {@link #BACK}, an app can provide a custom title
+ * and icon for the action. However, depending on the template the action belongs to, the title or
+ * icon may be disallowed. If such restrictions apply, the documentation of the APIs that consume
+ * the action will note them accordingly.
+ */
+public final class Action {
+    /**
+     * The type of action represented by the {@link Action } instance.
+     *
+     * @hide
+     */
+    // TODO(shiufai): investigate how to expose IntDefs if needed.
+    @RestrictTo(LIBRARY)
+    @IntDef(
+            value = {
+                    TYPE_UNKNOWN,
+                    TYPE_CUSTOM,
+                    TYPE_APP_ICON,
+                    TYPE_BACK,
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ActionType {
+    }
+
+    static final int FLAG_STANDARD = 1 << 16;
+
+    /**
+     * An unknown action type.
+     */
+    public static final int TYPE_UNKNOWN = 0;
+
+    /**
+     * An app-defined custom action type.
+     */
+    public static final int TYPE_CUSTOM = 1;
+
+    /**
+     * An action representing an app's icon.
+     *
+     * @see #APP_ICON
+     */
+    public static final int TYPE_APP_ICON = 2 | FLAG_STANDARD;
+
+    /**
+     * An action to navigate back in the user interface.
+     *
+     * @see #BACK
+     */
+    public static final int TYPE_BACK = 3 | FLAG_STANDARD;
+
+    /**
+     * A standard action to show the app's icon.
+     *
+     * <p>This action is non-interactive.
+     */
+    @NonNull
+    public static final Action APP_ICON = new Action(TYPE_APP_ICON);
+
+    /**
+     * A standard action to navigate back in the user interface.
+     *
+     * <p>The default behavior for a back press will call
+     * {@link androidx.car.app.ScreenManager#pop}.
+     *
+     * <p>To override the default behavior, register a {@link OnBackPressedCallback} via
+     * {@link OnBackPressedDispatcher#addCallback(LifecycleOwner, OnBackPressedCallback)}, which
+     * you can retrieve from {@link CarContext#getOnBackPressedDispatcher()}.
+     */
+    @NonNull
+    public static final Action BACK = new Action(TYPE_BACK);
+
+    @Keep
+    @Nullable
+    private final CarText mTitle;
+    @Keep
+    @Nullable
+    private final CarIcon mIcon;
+    @Keep
+    private final CarColor mBackgroundColor;
+    @Keep
+    @Nullable
+    private final OnClickListenerWrapper mListener;
+    @Keep
+    @ActionType
+    private final int mType;
+
+    /** Constructs a new builder of {@link Action}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns a {@link Builder} instance configured with the same data as this {@link Action}
+     * instance.
+     */
+    @NonNull
+    public Builder newBuilder() {
+        return new Builder(this);
+    }
+
+    /**
+     * Returns the title displayed in the action, or {@code null} if the action does not have a
+     * title.
+     */
+    @Nullable
+    public CarText getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * Returns the {@link CarIcon} to displayed in the action, or {@code null} if the action does
+     * not
+     * have an icon.
+     */
+    @Nullable
+    public CarIcon getIcon() {
+        return mIcon;
+    }
+
+    /**
+     * Returns the {@link CarColor} used for the background color of the action.
+     */
+    @NonNull
+    public CarColor getBackgroundColor() {
+        return mBackgroundColor;
+    }
+
+
+    @ActionType
+    public int getType() {
+        return mType;
+    }
+
+
+    public boolean isStandard() {
+        return isStandardActionType(mType);
+    }
+
+
+    @Nullable
+    public OnClickListenerWrapper getOnClickListener() {
+        return mListener;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[type: " + typeToString(mType) + ", icon: " + mIcon + ", bkg: " + mBackgroundColor
+                + "]";
+    }
+
+    /**
+     * Converts the given {@code type} into a string representation.
+     */
+    @NonNull
+    public static String typeToString(@ActionType int type) {
+        switch (type) {
+            case TYPE_CUSTOM:
+                return "CUSTOM";
+            case TYPE_APP_ICON:
+                return "APP_ICON";
+            case TYPE_BACK:
+                return "BACK";
+            default:
+                return "<unknown>";
+        }
+    }
+
+    /** Convenience constructor for standard action singletons. */
+    private Action(@ActionType int type) {
+        if (!isStandardActionType(type)) {
+            throw new IllegalArgumentException(
+                    "Standard action constructor used with non standard type");
+        }
+
+        mTitle = null;
+        mIcon = null;
+        mBackgroundColor = DEFAULT;
+
+        // The listeners can be set, for actions that support it, by copying the standard action
+        // instance with the newBuilder and setting it.
+        mListener = null;
+        this.mType = type;
+    }
+
+    private Action(
+            @Nullable CarText title,
+            @Nullable CarIcon icon,
+            CarColor backgroundColor,
+            @Nullable OnClickListenerWrapper listener,
+            @ActionType int type) {
+        this.mTitle = title;
+        this.mIcon = icon;
+        this.mBackgroundColor = backgroundColor;
+        this.mListener = listener;
+        this.mType = type;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private Action() {
+        mTitle = null;
+        mIcon = null;
+        mBackgroundColor = DEFAULT;
+        mListener = null;
+        mType = TYPE_UNKNOWN;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mTitle, mType, mListener == null, mIcon == null);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof Action)) {
+            return false;
+        }
+        Action otherAction = (Action) other;
+
+        // Don't compare callback, only ensure if it is present in one, it is also present in
+        // the other.
+        return Objects.equals(mTitle, otherAction.mTitle)
+                && mType == otherAction.mType
+                && Objects.equals(mIcon, otherAction.mIcon)
+                && Objects.equals(mListener == null, otherAction.mListener == null);
+    }
+
+    private static boolean isStandardActionType(@ActionType int type) {
+        return 0 != (type & FLAG_STANDARD);
+    }
+
+    /** A builder of {@link Action}. */
+    public static final class Builder {
+        @Nullable
+        private CarText mTitle;
+        @Nullable
+        private CarIcon mIcon;
+        @Nullable
+        private OnClickListenerWrapper mListener;
+        private CarColor mBackgroundColor = DEFAULT;
+        @ActionType
+        private int mType = TYPE_CUSTOM;
+
+        /**
+         * Sets the title to display in the action, or {@code null} to not display a title.
+         *
+         * <p>The title of a standard action can be set with this method. Actions, including
+         * standard
+         * actions, don't have a title by default.
+         */
+        @NonNull
+        public Builder setTitle(@Nullable CharSequence title) {
+            this.mTitle = title == null ? null : CarText.create(title);
+            return this;
+        }
+
+        /**
+         * Sets the icon to display in the action, or {@code null} to not display an icon.
+         *
+         * <p>Icons can't be set in standard actions.
+         *
+         * <h4>Icon Sizing Guidance</h4>
+         *
+         * The provided icon should have a maximum size of 36 x 36 dp. If the icon exceeds this
+         * maximum
+         * size in either one of the dimensions, it will be scaled down to be centered inside the
+         * bounding box while preserving the aspect ratio.
+         *
+         * <p>See {@link CarIcon} for more details related to providing icon and image resources
+         * that
+         * work with different car screen pixel densities.
+         */
+        @NonNull
+        public Builder setIcon(@Nullable CarIcon icon) {
+            CarIconConstraints.DEFAULT.validateOrThrow(icon);
+            this.mIcon = icon;
+            return this;
+        }
+
+        /** Sets the {@link OnClickListener} to call when the action is clicked. */
+        @NonNull
+        @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+        public Builder setOnClickListener(@Nullable OnClickListener listener) {
+            this.mListener = listener == null ? null : OnClickListenerWrapper.create(listener);
+            return this;
+        }
+
+        /**
+         * Sets the background color to be used for the action.
+         *
+         * <h4>Requirements</h4>
+         *
+         * <p>The host may ignore this color and use the default instead if the color does not
+         * pass the
+         * contrast requirements.
+         *
+         * @param backgroundColor the {@link CarColor} to set as background. Use {@link
+         *                        CarColor#DEFAULT} to let the host pick a default.
+         * @throws IllegalArgumentException if {@code backgroundColor} is not a standard color.
+         * @throws NullPointerException     if {@code backgroundColor} is {@code null}.
+         */
+        @NonNull
+        public Builder setBackgroundColor(@NonNull CarColor backgroundColor) {
+            STANDARD_ONLY.validateOrThrow(requireNonNull(backgroundColor));
+            this.mBackgroundColor = backgroundColor;
+            return this;
+        }
+
+        /**
+         * Constructs the {@link Action} defined by this builder.
+         *
+         * @throws IllegalStateException if the action is not a standard action and does not have an
+         *                               icon or a title.
+         * @throws IllegalStateException if a listener is set on either {@link #APP_ICON} or {@link
+         *                               #BACK}.
+         * @throws IllegalStateException if an icon or title is set on either {@link #APP_ICON} or
+         *                               {@link #BACK}.
+         */
+        @NonNull
+        public Action build() {
+            if (mType == TYPE_UNKNOWN) {
+                throw new IllegalStateException("Missing action type");
+            }
+            boolean isStandard = isStandardActionType(mType);
+            if (!isStandard && mIcon == null && (mTitle == null || TextUtils.isEmpty(
+                    mTitle.getText()))) {
+                throw new IllegalStateException("An action must have either an icon or a title");
+            }
+
+            if ((mType == TYPE_APP_ICON || mType == TYPE_BACK)) {
+                if (mListener != null) {
+                    throw new IllegalStateException(
+                            "An on-click listener can't be set on the standard back or app-icon "
+                                    + "action");
+                }
+
+                if (mIcon != null || (mTitle != null && !TextUtils.isEmpty(mTitle.getText()))) {
+                    throw new IllegalStateException(
+                            "An icon or title can't be set on the standard back or app-icon "
+                                    + "action");
+                }
+            }
+
+            return new Action(mTitle, mIcon, mBackgroundColor, mListener, mType);
+        }
+
+        private Builder() {
+        }
+
+        private Builder(Action action) {
+            mTitle = action.mTitle;
+            mIcon = action.mIcon;
+            mBackgroundColor = action.mBackgroundColor;
+            mListener = action.mListener;
+            mType = action.mType;
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ActionList.java b/car/app/app/src/main/java/androidx/car/app/model/ActionList.java
new file mode 100644
index 0000000..99f5bdf
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ActionList.java
@@ -0,0 +1,92 @@
+/*
+ * 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.car.app.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a simple list of {@link Action} models.
+ *
+ * <p>This model is intended for internal and host use only, as a transport artifact for
+ * homogeneous lists of {@link Action} items.
+ */
+public class ActionList {
+    private final List<Action> mList;
+
+    /**
+     * Returns the list of {@link Action}'s.
+     */
+    @NonNull
+    public List<Action> getList() {
+        return mList;
+    }
+
+    /**
+     * Creates an {@link ActionList} instance based on the list of {@link Action}'s.
+     */
+    @NonNull
+    public static ActionList create(@NonNull List<Action> list) {
+        requireNonNull(list);
+        for (Action action : list) {
+            if (action == null) {
+                throw new IllegalArgumentException("Disallowed null action found in action list");
+            }
+        }
+        return new ActionList(list);
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return mList.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mList);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof ActionList)) {
+            return false;
+        }
+        ActionList otherActionList = (ActionList) other;
+
+        return Objects.equals(mList, otherActionList.mList);
+    }
+
+    private ActionList(List<Action> list) {
+        this.mList = new ArrayList<>(list);
+    }
+
+    /** For serialization. */
+    private ActionList() {
+        mList = Collections.emptyList();
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java b/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java
new file mode 100644
index 0000000..19ee2d5
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java
@@ -0,0 +1,160 @@
+/*
+ * 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.car.app.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.Action.ActionType;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents a list of {@link Action}s that are used for a template.
+ *
+ * <p>The {@link Action}s in the {@link ActionStrip} may be displayed differently depending on the
+ * template they are used with. For example, a map template may display them as a group of floating
+ * action buttons (FABs) over the map background.
+ *
+ * <p>See the documentation of individual {@link Template}s on restrictions around what actions are
+ * supported.
+ */
+public class ActionStrip {
+    @Keep
+    private final List<Object> mActions;
+
+    /** Constructs a new builder of {@link ActionStrip}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns the list of {@link Action}'s.
+     */
+    @NonNull
+    public List<Object> getActions() {
+        return mActions;
+    }
+
+    /**
+     * Returns the {@link Action} associated with the input {@code actionType}, or {@code null} if
+     * no matching {@link Action} is found.
+     */
+    @Nullable
+    public Action getActionOfType(@ActionType int actionType) {
+        for (Object object : mActions) {
+            if (object instanceof Action) {
+                Action action = (Action) object;
+                if (action.getType() == actionType) {
+                    return action;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[action count: " + mActions.size() + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mActions);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof ActionStrip)) {
+            return false;
+        }
+        ActionStrip otherActionStrip = (ActionStrip) other;
+
+        return Objects.equals(mActions, otherActionStrip.mActions);
+    }
+
+    private ActionStrip(Builder builder) {
+        mActions = builder.mActions;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private ActionStrip() {
+        mActions = Collections.emptyList();
+    }
+
+    /** A builder of {@link ActionStrip}. */
+    public static final class Builder {
+        private final List<Object> mActions = new ArrayList<>();
+        private final Set<Integer> mAddedActionTypes = new HashSet<>();
+
+        /**
+         * Adds an {@link Action} to the list.
+         *
+         * @throws IllegalArgumentException if {@code action} is a standard action and an action of
+         *                                  the same type has already been added.
+         * @throws NullPointerException     if {@code action} is {@code null}.
+         */
+        @NonNull
+        public Builder addAction(@NonNull Action action) {
+            int actionType = requireNonNull(action).getType();
+            if (actionType != Action.TYPE_CUSTOM && mAddedActionTypes.contains(actionType)) {
+                throw new IllegalArgumentException(
+                        "Duplicated action types are disallowed: " + action);
+            }
+            mAddedActionTypes.add(actionType);
+            mActions.add(action);
+            return this;
+        }
+
+        /**
+         * Clears any actions that may have been added with {@link #addAction(Action)} up to this
+         * point.
+         */
+        @NonNull
+        public Builder clearActions() {
+            mActions.clear();
+            mAddedActionTypes.clear();
+            return this;
+        }
+
+        /**
+         * Constructs the {@link ActionStrip} defined by this builder.
+         *
+         * @throws IllegalStateException if the action strip is empty.
+         */
+        @NonNull
+        public ActionStrip build() {
+            if (mActions.isEmpty()) {
+                throw new IllegalStateException("Action strip must contain at least one action");
+            }
+            return new ActionStrip(this);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/CarColor.java b/car/app/app/src/main/java/androidx/car/app/model/CarColor.java
new file mode 100644
index 0000000..24020e2
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/CarColor.java
@@ -0,0 +1,323 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+// TODO(shiufai): Add link to color guidelines.
+
+/**
+ * Represents a color to be used in a car app.
+ *
+ * <p>The host chooses the dark or light variant of the color when displaying the user interface,
+ * depending where the color is used, to ensure the proper contrast ratio is maintained. For
+ * example, the dark variant when may be used as the background of a view with brighter text on it,
+ * and the light variant for text on a dark background.
+ *
+ * <p>Colors provided by the app should meet the contrast requirements defined by the host, and
+ * documented by the app quality guidelines.
+ *
+ * <h4>Standard colors</h4>
+ *
+ * A set of standard {@link CarColor} instances (for example, {@link #BLUE}) is available in this
+ * class. It is recommended to use these standard colors whenever possible as they are guaranteed to
+ * adhere to the contrast requirements.
+ *
+ * <h4>Primary and secondary colors</h4>
+ *
+ * The app can define two additional {@link CarColor}s in its manifest metadata, through the <code>
+ * carColorPrimary</code>, <code>carColorPrimaryDark</code>, <code>
+ * carColorSecondary</code>, and <code>carColorSecondaryDark</code> theme attributes, by declaring
+ * them in a theme and referencing the theme from the <code>
+ * androidx.car.app.theme</code> metadata. Both the light and dark variants must
+ * be declared for the primary and secondary colors, otherwise default variants will be used.
+ * Wherever primary and secondary colors are used by the app, the host may use a default color
+ * instead if the colors do not pass the contrast requirements.
+ *
+ * <p>In <code>AndroidManifest.xml</code>, under the <code>application</code> element corresponding
+ * to the car app:
+ *
+ * <pre>{@code
+ * <meta-data
+ *   android:name="androidx.car.app.theme"
+ *   android:resource="@style/CarAppTheme"/>
+ * }</pre>
+ *
+ * The <code>CarAppTheme</code> style is defined as any other themes in a resource file:
+ *
+ * <pre>{@code
+ * <resources>
+ *   <style name="CarAppTheme">
+ *     <item name="carColorPrimary">@color/my_primary_car_color</item>
+ *     <item name="carColorPrimaryDark">@color/my_primary_dark_car_color</item>
+ *     <item name="carColorSecondary">@color/my_secondary_car_color</item>
+ *     <item name="carColorSecondaryDark">@color/my_secondary_cark_car_color</item>
+ *   </style>
+ * </resources>
+ * }</pre>
+ *
+ * <h4>Custom Colors</h4>
+ *
+ * Besides the primary and secondary colors, custom colors can be created at runtime with {@link
+ * #createCustom}. Wherever custom colors are used by the app, the host may use a default color
+ * instead if the custom color does not pass the contrast requirements.
+ */
+public class CarColor {
+    /**
+     * The type of color represented by the {@link CarColor} instance.
+     *
+     * @hide
+     */
+    // TODO(shiufai): investigate how to expose IntDefs if needed.
+    @IntDef(
+            value = {
+                    TYPE_CUSTOM,
+                    TYPE_DEFAULT,
+                    TYPE_PRIMARY,
+                    TYPE_SECONDARY,
+                    TYPE_RED,
+                    TYPE_GREEN,
+                    TYPE_BLUE,
+                    TYPE_YELLOW
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY)
+    public @interface CarColorType {
+    }
+
+    /**
+     * A custom, non-standard, app-defined color.
+     */
+    @CarColorType
+    public static final int TYPE_CUSTOM = 0;
+
+    /**
+     * A default color, chosen by the host.
+     *
+     * @see #DEFAULT
+     */
+    @CarColorType
+    public static final int TYPE_DEFAULT = 1;
+
+    /**
+     * The primary app color.
+     *
+     * @see #PRIMARY
+     */
+    @CarColorType
+    public static final int TYPE_PRIMARY = 2;
+
+    /**
+     * The secondary app color.
+     *
+     * @see #SECONDARY
+     */
+    @CarColorType
+    public static final int TYPE_SECONDARY = 3;
+
+    /**
+     * The standard red color.
+     *
+     * @see #RED
+     */
+    @CarColorType
+    public static final int TYPE_RED = 4;
+
+    /**
+     * The standard green color.
+     *
+     * @see #GREEN
+     */
+    @CarColorType
+    public static final int TYPE_GREEN = 5;
+
+    /**
+     * The standard blue color.
+     *
+     * @see #BLUE
+     */
+    @CarColorType
+    public static final int TYPE_BLUE = 6;
+
+    /**
+     * The standard yellow color.
+     *
+     * @see #YELLOW
+     */
+    @CarColorType
+    public static final int TYPE_YELLOW = 7;
+
+    /**
+     * Indicates that a default color should be used.
+     *
+     * <p>This can be used for example to tell the host that the app has no preference for the
+     * tint of
+     * an icon, and it should use whatever default it finds appropriate.
+     */
+    @NonNull
+    public static final CarColor DEFAULT = create(TYPE_DEFAULT);
+
+    /**
+     * Indicates that the app primary color and its dark version should be used, as declared in the
+     * app manifest through the <code>carColorPrimary</code> and <code>carColorPrimaryDark</code>
+     * theme attributes.
+     */
+    @NonNull
+    public static final CarColor PRIMARY = create(TYPE_PRIMARY);
+
+    /**
+     * Indicates that the app secondary color and its dark version should be used, as declared in
+     * the
+     * app manifest through the <code>carColorSecondary</code> and
+     * <code>carColorSecondaryDark</code>
+     * theme attributes.
+     */
+    @NonNull
+    public static final CarColor SECONDARY = create(TYPE_SECONDARY);
+
+    /** A standard red color. */
+    @NonNull
+    public static final CarColor RED = create(TYPE_RED);
+
+    /** A standard green color. */
+    @NonNull
+    public static final CarColor GREEN = create(TYPE_GREEN);
+
+    /** A standard blue color. */
+    @NonNull
+    public static final CarColor BLUE = create(TYPE_BLUE);
+
+    /** A standard yellow color. */
+    @NonNull
+    public static final CarColor YELLOW = create(TYPE_YELLOW);
+
+    @Keep
+    @CarColorType
+    private final int mType;
+
+    /** A light-variant custom color-int, used when the type is {@link #TYPE_CUSTOM}. */
+    @Keep
+    @ColorInt
+    private final int mColor;
+
+    /** A dark-variant custom color-int, used when the type is {@link #TYPE_CUSTOM}. */
+    @Keep
+    @ColorInt
+    private final int mColorDark;
+
+    /**
+     * Returns an instance of {@link CarColor} containing a non-standard color.
+     *
+     * <p>See the top-level documentation of {@link CarColor} for details about how the host
+     * determines which variant is used.
+     */
+    @NonNull
+    public static CarColor createCustom(@ColorInt int color, @ColorInt int colorDark) {
+        return new CarColor(TYPE_CUSTOM, color, colorDark);
+    }
+
+    @CarColorType
+    public int getType() {
+        return mType;
+    }
+
+    @ColorInt
+    public int getColor() {
+        return mColor;
+    }
+
+    @ColorInt
+    public int getColorDark() {
+        return mColorDark;
+    }
+
+    @Override
+    public String toString() {
+        return "[type: " + typeToString(mType) + ", color: " + mColor + ", dark: " + mColorDark
+                + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mType, mColor, mColorDark);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof CarColor)) {
+            return false;
+        }
+        CarColor otherColor = (CarColor) other;
+
+        return mColor == otherColor.mColor
+                && mColorDark == otherColor.mColorDark
+                && mType == otherColor.mType;
+    }
+
+    private static CarColor create(@CarColorType int type) {
+        return new CarColor(type, 0, 0);
+    }
+
+    private static String typeToString(@CarColorType int type) {
+        switch (type) {
+            case TYPE_BLUE:
+                return "BLUE";
+            case TYPE_DEFAULT:
+                return "DEFAULT";
+            case TYPE_PRIMARY:
+                return "PRIMARY";
+            case TYPE_SECONDARY:
+                return "SECONDARY";
+            case TYPE_CUSTOM:
+                return "CUSTOM";
+            case TYPE_GREEN:
+                return "GREEN";
+            case TYPE_RED:
+                return "RED";
+            case TYPE_YELLOW:
+                return "YELLOW";
+            default:
+                return "<unknown>";
+        }
+    }
+
+    private CarColor() {
+        mType = TYPE_DEFAULT;
+        mColor = 0;
+        mColorDark = 0;
+    }
+
+    private CarColor(@CarColorType int type, @ColorInt int color, @ColorInt int colorDark) {
+        this.mType = type;
+        this.mColor = color;
+        this.mColorDark = colorDark;
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java b/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java
new file mode 100644
index 0000000..ae04556
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java
@@ -0,0 +1,441 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.model.CarColor.DEFAULT;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.ContentResolver;
+import android.graphics.PorterDuff.Mode;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.constraints.CarColorConstraints;
+import androidx.car.app.model.constraints.CarIconConstraints;
+import androidx.core.graphics.drawable.IconCompat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represents an icon to be used in a car app.
+ *
+ * <p>Car icons wrap a backing {@link IconCompat}, and add additional attributes optimized for the
+ * car such as a {@link CarColor} tint.
+ *
+ * <h4>Car Screen Pixel Densities</h4>
+ *
+ * <p>Similar to Android devices, car screens cover a wide range of pixel densities. To ensure that
+ * icons and images render well across all car screens, use vector assets whenever possible to avoid
+ * scaling issues.
+ *
+ * <p>In order to support all car screen sizes and pixel density, you can use configuration
+ * qualifiers in your resource files (e.g. "mdpi", "hdpi", etc). See
+ * {@link androidx.car.app.CarContext} for more details.
+ *
+ * <h4>Themed Drawables</h4>
+ *
+ * Vector drawables can contain references to attributes declared in a theme. For example:
+ *
+ * <pre>{@code
+ * <vector ...
+ *   <path
+ *     android:pathData="..."
+ *     android:fillColor="?myIconColor"/>
+ * </vector>
+ * }</pre>
+ *
+ * The theme must be defined in the app's manifest metadata, by declaring them in a theme and
+ * referencing it from the <code>androidx.car.app.theme</code> metadata.
+ *
+ * <p>In <code>AndroidManifest.xml</code>, under the <code>application</code> element corresponding
+ * to the car app:
+ *
+ * <pre>{@code
+ * <meta-data
+ *   android:name="androidx.car.app.theme"
+ *   android:resource="@style/CarAppTheme"/>
+ * }</pre>
+ *
+ * The <code>CarAppTheme</code> style is defined as any other themes in a resource file:
+ *
+ * <pre>{@code
+ * <resources>
+ *   <style name="CarAppTheme">
+ *     <item name="myIconColor">@color/my_icon_color</item>
+ *     ...
+ *   </style>
+ * </resources>
+ * }</pre>
+ */
+public class CarIcon {
+    /** Matches with {@link android.graphics.drawable.Icon#TYPE_RESOURCE} */
+    private static final int TYPE_RESOURCE = 2;
+
+    /** Matches with {@link android.graphics.drawable.Icon#TYPE_URI} */
+    private static final int TYPE_URI = 4;
+
+    /**
+     * The type of car icon represented by the {@link CarIcon} instance.
+     *
+     * @hide
+     */
+    // TODO(shiufai): investigate how to expose IntDefs if needed.
+    @RestrictTo(LIBRARY)
+    @IntDef(
+            value = {
+                    TYPE_CUSTOM,
+                    TYPE_BACK,
+                    TYPE_ALERT,
+                    TYPE_APP,
+                    TYPE_ERROR,
+                    TYPE_WILLIAM_ALERT,
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CarIconType {
+    }
+
+    /**
+     * An unknown icon type.
+     */
+    public static final int TYPE_UNKNOWN = 0;
+
+    /**
+     * A custom, non-standard, app-defined icon.
+     */
+    public static final int TYPE_CUSTOM = 1;
+
+    /**
+     * An icon representing a "back" action.
+     *
+     * @see #BACK
+     */
+    public static final int TYPE_BACK = 3;
+
+    /**
+     * An alert icon.
+     *
+     * @see #ALERT
+     */
+    public static final int TYPE_ALERT = 4;
+
+    /**
+     * The app's icon.
+     *
+     * @see #APP_ICON
+     */
+    public static final int TYPE_APP = 5;
+
+    /**
+     * An error icon.
+     *
+     * @see #ERROR
+     */
+    public static final int TYPE_ERROR = 6;
+
+    /**
+     * An alerting William.
+     *
+     * @see #WILLIAM_ALERT
+     */
+    public static final int TYPE_WILLIAM_ALERT = 7;
+
+    /**
+     * Represents the app's icon, as defined in the app's manifest by the {@code android:icon}
+     * attribute of the {@code application} element.
+     */
+    @NonNull
+    public static final CarIcon APP_ICON = CarIcon.forStandardType(TYPE_APP);
+
+    @NonNull
+    public static final CarIcon BACK = CarIcon.forStandardType(TYPE_BACK);
+
+    @NonNull
+    public static final CarIcon ALERT = CarIcon.forStandardType(TYPE_ALERT);
+
+    @NonNull
+    public static final CarIcon ERROR = CarIcon.forStandardType(TYPE_ERROR);
+
+    @NonNull
+    public static final CarIcon WILLIAM_ALERT =
+            CarIcon.forStandardType(TYPE_WILLIAM_ALERT, /* tint= */ null);
+
+    @Keep
+    @CarIconType
+    private final int mType;
+    @Keep
+    @Nullable
+    private final IconCompat mIcon;
+    @Keep
+    @Nullable
+    private final CarColor mTint;
+
+    @Nullable
+    public IconCompat getIcon() {
+        return mIcon;
+    }
+
+    @Nullable
+    public CarColor getTint() {
+        return mTint;
+    }
+
+    @CarIconType
+    public int getType() {
+        return mType;
+    }
+
+    @Override
+    public String toString() {
+        return "[type: " + typeToString(mType) + ", tint: " + mTint + "]";
+    }
+
+    /**
+     * Returns a {@link Builder} instance configured with the same data as this {@link CarIcon}
+     * instance.
+     */
+    @NonNull
+    public Builder newBuilder() {
+        return new Builder(this);
+    }
+
+    /**
+     * Returns a {@link Builder} instance using the given {@link IconCompat}.
+     *
+     * <p>The following types are supported:
+     *
+     * <ul>
+     *   <li>{@link IconCompat#TYPE_BITMAP}
+     *   <li>{@link IconCompat#TYPE_RESOURCE}
+     *   <li>{@link IconCompat#TYPE_URI}
+     * </ul>
+     *
+     * <p>{@link IconCompat#TYPE_URI} is only supported in templates that explicitly allow it. In
+     * those cases, the appropriate APIs will be documented to indicate this.
+     *
+     * <p>For {@link IconCompat#TYPE_URI}, the URI's scheme must be {@link
+     * ContentResolver#SCHEME_CONTENT}.
+     *
+     * <p>If the icon image is loaded from URI, it may be cached on the host side. Changing the
+     * contents of the URI will result in the host showing a stale image.
+     *
+     * @throws IllegalArgumentException if {@code icon}'s URI scheme is not supported.
+     * @throws NullPointerException     if {@code icon} is {@code null}.
+     */
+    @NonNull
+    public static Builder builder(@NonNull IconCompat icon) {
+        return new Builder(
+                CarIconConstraints.UNCONSTRAINED.checkSupportedIcon(requireNonNull(icon)));
+    }
+
+    /**
+     * Returns a {@link CarIcon} instance wrapping the given {@link IconCompat}.
+     *
+     * @throws IllegalArgumentException if {@code icon}'s type is not supported.
+     * @throws NullPointerException     if {@code icon} is {@code null}.
+     * @see #builder(IconCompat)
+     */
+    @NonNull
+    public static CarIcon of(@NonNull IconCompat icon) {
+        return builder(requireNonNull(icon)).setTint(null).build();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mType, mTint, iconCompatHash());
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof CarIcon)) {
+            return false;
+        }
+        CarIcon otherIcon = (CarIcon) other;
+
+        return mType == otherIcon.mType
+                && Objects.equals(mTint, otherIcon.mTint)
+                && iconCompatEquals(otherIcon.mIcon);
+    }
+
+    @Nullable
+    private Object iconCompatHash() {
+        // Use the same things being compared in iconCompatEquals for hashing.
+        if (mIcon == null) {
+            return null;
+        }
+
+        int type = mIcon.getType();
+        if (type == TYPE_RESOURCE) {
+            return mIcon.getResPackage() + mIcon.getResId();
+        } else if (type == TYPE_URI) {
+            return mIcon.getUri();
+        }
+
+        return VERSION.SDK_INT >= VERSION_CODES.M;
+    }
+
+    private boolean iconCompatEquals(@Nullable IconCompat other) {
+        if (mIcon == null) {
+            return other == null;
+        } else if (other == null) {
+            return false;
+        }
+
+        int type = mIcon.getType();
+        int otherType = other.getType();
+
+        if (type != otherType) {
+            return false;
+        }
+
+        // TODO(shiufai): Decide how/if we will diff bitmap type IconCompat
+        if (type == TYPE_RESOURCE) {
+            return Objects.equals(mIcon.getResPackage(), other.getResPackage())
+                    && mIcon.getResId() == other.getResId();
+        } else if (type == TYPE_URI) {
+            return Objects.equals(mIcon.getUri(), other.getUri());
+        }
+
+        // Before Android version M, we support a subset of image types (resource or uri), so we
+        // compare the instances' resource info or uri to check for equality. For M or above,
+        // since we support any icon types, we only check for type equality if the type is
+        // neither a resource or uri.
+        return VERSION.SDK_INT >= VERSION_CODES.M;
+    }
+
+    private static CarIcon forStandardType(@CarIconType int type) {
+        return forStandardType(type, DEFAULT);
+    }
+
+    private static CarIcon forStandardType(@CarIconType int type, @Nullable CarColor tint) {
+        return new CarIcon(null, tint, type);
+    }
+
+    private static String typeToString(@CarIconType int type) {
+        switch (type) {
+            case TYPE_ALERT:
+                return "ALERT";
+            case TYPE_APP:
+                return "APP";
+            case TYPE_ERROR:
+                return "ERROR";
+            case TYPE_WILLIAM_ALERT:
+                return "WILLIAM_ALERT";
+            case TYPE_BACK:
+                return "BACK";
+            case TYPE_CUSTOM:
+                return "CUSTOM";
+            default:
+                return "<unknown>";
+        }
+    }
+
+    private CarIcon(@Nullable IconCompat icon, @Nullable CarColor tint, @CarIconType int type) {
+        this.mType = type;
+        this.mIcon = icon;
+        this.mTint = tint;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private CarIcon() {
+        this.mType = TYPE_UNKNOWN;
+        this.mIcon = null;
+        this.mTint = null;
+    }
+
+    /** A builder of {@link CarIcon}. */
+    public static final class Builder {
+        @Nullable
+        private IconCompat mIcon;
+        @Nullable
+        private CarColor mTint;
+        @CarIconType
+        private int mType;
+
+        /**
+         * Configures the builder with the same icon and tint as the given {@link CarIcon}.
+         *
+         * @throws NullPointerException if {@code carIcon} is {@code null}.
+         */
+        @NonNull
+        public Builder setIcon(@NonNull CarIcon carIcon) {
+            requireNonNull(carIcon);
+            mIcon = carIcon.getIcon();
+            mTint = carIcon.getTint();
+            mType = carIcon.getType();
+            return this;
+        }
+
+        /**
+         * Sets the tint of the icon to the given {@link CarColor}.
+         *
+         *
+         * <p>This tint overrides the tint set through {@link IconCompat#setTint(int)} in the
+         * backing
+         * {@link IconCompat} with a {@link CarColor} tint.The tint set through {@link
+         * IconCompat#setTint(int)} is not guaranteed to be applied if the {@link CarIcon} tint
+         * is not
+         * set.
+         *
+         * <p>The tint mode used to blend this color is {@link Mode#SRC_IN}.
+         *
+         * <p>If set to {@code null}, then no tint will be applied to the icon.
+         *
+         * <p>By default, no tint is set unless one is specified with this method.
+         *
+         * @see CarColor
+         * @see android.graphics.drawable.Drawable#setTintMode(Mode)
+         */
+        @NonNull
+        public Builder setTint(@Nullable CarColor tint) {
+            if (tint != null) {
+                CarColorConstraints.UNCONSTRAINED.validateOrThrow(tint);
+            }
+            this.mTint = tint;
+            return this;
+        }
+
+        /** Constructs the {@link CarIcon} defined by this builder. */
+        @NonNull
+        public CarIcon build() {
+            return new CarIcon(mIcon, mTint, mType);
+        }
+
+        private Builder(@NonNull IconCompat icon) {
+            mType = TYPE_CUSTOM;
+            this.mIcon = icon;
+            mTint = null;
+        }
+
+        private Builder(@NonNull CarIcon carIcon) {
+            mType = carIcon.getType();
+            mIcon = carIcon.getIcon();
+            mTint = carIcon.getTint();
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/CarIconSpan.java b/car/app/app/src/main/java/androidx/car/app/model/CarIconSpan.java
new file mode 100644
index 0000000..55c9163
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/CarIconSpan.java
@@ -0,0 +1,214 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.text.TextPaint;
+import android.text.style.CharacterStyle;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.constraints.CarIconConstraints;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A span that replaces the text it is attached to with a {@link CarIcon} that is aligned with the
+ * surrounding text.
+ *
+ * <p>The image may be scaled with the text differently depending on the template that the text
+ * belongs to. Refer to the documentation of each template for that information.
+ *
+ * <p>For example, the following code creates a string for a navigation maneuver that has an image
+ * with the number of a highway rendered as an icon in between "on" and "East":
+ *
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Turn right on 520 East");
+ * string.setSpan(
+ *     CarIconSpan.create(CarIcon.of(
+ *         IconCompat.createWithResource(getCarContext(), R.drawable.ic_520_highway))),
+ *         14, 17, SPAN_INCLUSIVE_EXCLUSIVE);
+ * }</pre>
+ *
+ * <p>{@link CarIconSpan}s in strings passed to the library templates may be ignored by the host
+ * when displaying the text unless support for them is explicitly documented in the API that takes
+ * the string.
+ *
+ * <p>This span will be ignored if it overlaps with any span that replaces text, such as another
+ * {@link DistanceSpan}, {@link DurationSpan}, or {@link CarIconSpan}.
+ *
+ * @see CarIcon
+ */
+public class CarIconSpan extends CharacterStyle {
+    /**
+     * Indicates how to align a car icon span with its surrounding text.
+     *
+     * @hide
+     */
+    @IntDef(
+            value = {
+                    ALIGN_CENTER,
+                    ALIGN_BOTTOM,
+                    ALIGN_BASELINE,
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    // TODO(shiufai): investigate how to expose IntDefs if needed.
+    @RestrictTo(LIBRARY)
+    public @interface Alignment {
+    }
+
+    /**
+     * A constant indicating that the bottom of this span should be aligned with the bottom of the
+     * surrounding text, at the same level as the lowest descender in the text.
+     */
+    @Alignment
+    public static final int ALIGN_BOTTOM = 0;
+
+    /**
+     * A constant indicating that the bottom of this span should be aligned with the baseline of the
+     * surrounding text.
+     */
+    @Alignment
+    public static final int ALIGN_BASELINE = 1;
+
+    /**
+     * A constant indicating that this span should be vertically centered between the top and the
+     * lowest descender.
+     */
+    @Alignment
+    public static final int ALIGN_CENTER = 2;
+
+    @Nullable
+    @Keep
+    private final CarIcon mIcon;
+    @Alignment
+    @Keep
+    private final int mAlignment;
+
+    /**
+     * Creates a {@link CarIconSpan} from a {@link CarIcon} with a default alignment of {@link
+     * #ALIGN_BASELINE}.
+     *
+     * @throws NullPointerException if {@code icon} is {@code null}.
+     * @see #create(CarIcon, int)
+     */
+    @NonNull
+    public static CarIconSpan create(@NonNull CarIcon icon) {
+        return create(icon, ALIGN_BASELINE);
+    }
+
+    /**
+     * Creates a {@link CarIconSpan} from a {@link CarIcon}, specifying the alignment of the icon
+     * with
+     * respect to its surrounding text.
+     *
+     * @param icon      the {@link CarIcon} to replace the text with.
+     * @param alignment the alignment of the {@link CarIcon} relative to the text. This should be
+     *                  one of {@link #ALIGN_BASELINE}, {@link #ALIGN_BOTTOM} or
+     *                  {@link #ALIGN_CENTER}.
+     * @throws NullPointerException     if {@code icon} is {@code null}.
+     * @throws IllegalArgumentException if {@code alignment} is not a valid value.
+     * @see #ALIGN_BASELINE
+     * @see #ALIGN_BOTTOM
+     * @see #ALIGN_CENTER
+     */
+    @NonNull
+    public static CarIconSpan create(@NonNull CarIcon icon, @Alignment int alignment) {
+        CarIconConstraints.DEFAULT.validateOrThrow(icon);
+        return new CarIconSpan(requireNonNull(icon), validateAlignment(alignment));
+    }
+
+    /**
+     * Ensures that the {@code alignment} is of one of the supported types.
+     */
+    public static int validateAlignment(int alignment) {
+        if (alignment != ALIGN_BASELINE && alignment != ALIGN_BOTTOM && alignment != ALIGN_CENTER) {
+            throw new IllegalStateException("Invalid alignment value: " + alignment);
+        }
+        return alignment;
+    }
+
+    private CarIconSpan(@Nullable CarIcon icon, @Alignment int alignment) {
+        this.mIcon = icon;
+        this.mAlignment = alignment;
+    }
+
+    private CarIconSpan() {
+        mIcon = null;
+        mAlignment = ALIGN_BASELINE;
+    }
+
+    @Nullable
+    public CarIcon getIcon() {
+        return mIcon;
+    }
+
+    @Alignment
+    public int getAlignment() {
+        return mAlignment;
+    }
+
+    @Override
+    public void updateDrawState(@Nullable TextPaint paint) {
+        // Not relevant.
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[icon: " + mIcon + ", alignment: " + alignmentToString(mAlignment) + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mIcon);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof CarIconSpan)) {
+            return false;
+        }
+        CarIconSpan otherIconSpan = (CarIconSpan) other;
+
+        return Objects.equals(mIcon, otherIconSpan.mIcon);
+    }
+
+    private static String alignmentToString(@Alignment int alignment) {
+        switch (alignment) {
+            case ALIGN_BASELINE:
+                return "baseline";
+            case ALIGN_BOTTOM:
+                return "bottom";
+            case ALIGN_CENTER:
+                return "center";
+            default:
+                return "unknown";
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/CarText.java b/car/app/app/src/main/java/androidx/car/app/model/CarText.java
new file mode 100644
index 0000000..a1b846a
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/CarText.java
@@ -0,0 +1,196 @@
+/*
+ * 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.car.app.model;
+
+import android.text.Spanned;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.utils.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A model used to send text with attached spans to the host.
+ */
+public class CarText {
+    /** An empty CarText for convenience. */
+    @NonNull
+    public static final CarText EMPTY = CarText.create("");
+
+    @Keep
+    @Nullable
+    private String mText;
+    @Keep
+    private final List<SpanWrapper> mSpans;
+
+    /**
+     * Returns {@code true} if the {@code carText} is {@code null} or an empty string, {@code
+     * false} otherwise.
+     */
+    public static boolean isNullOrEmpty(@Nullable CarText carText) {
+        if (carText == null) {
+            return true;
+        }
+
+        String text = carText.mText;
+        return text == null || text.isEmpty();
+    }
+
+    /**
+     * Returns a {@link CarText} instance for the given {@link CharSequence}, by sanitizing the car
+     * sequence (dropping unsupported {@link Spanned} objects, and wrapping the remaining supported
+     * {@link Spanned} objects into data that can be sent across to the host in a bundle.
+     */
+    @NonNull
+    public static CarText create(@NonNull CharSequence text) {
+        return new CarText(text);
+    }
+
+    @Nullable
+    public String getText() {
+        return mText;
+    }
+
+    public boolean isEmpty() {
+        return mText == null || mText.isEmpty();
+    }
+
+    /** Returns the optional list of spans attached to the text. */
+    @NonNull
+    public List<SpanWrapper> getSpans() {
+        return mSpans;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        String text = getText();
+        return text == null ? "" : text;
+    }
+
+    /**
+     * Returns a shortened string from the input {@code text}.
+     */
+    @Nullable
+    public static String toShortString(@Nullable CarText text) {
+        return text == null ? null : StringUtils.shortenString(text.toString());
+    }
+
+    public CarText() {
+        mText = null;
+        mSpans = Collections.emptyList();
+    }
+
+    private CarText(CharSequence text) {
+        this.mText = text.toString();
+
+        mSpans = new ArrayList<>();
+
+        if (text instanceof Spanned) {
+            Spanned spanned = (Spanned) text;
+
+            for (Object span : spanned.getSpans(0, text.length(), Object.class)) {
+                if (span instanceof ForegroundCarColorSpan
+                        || span instanceof CarIconSpan
+                        || span instanceof DurationSpan
+                        || span instanceof DistanceSpan) {
+                    mSpans.add(SpanWrapper.wrap(spanned, span));
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof CarText)) {
+            return false;
+        }
+        CarText otherText = (CarText) other;
+        return Objects.equals(mText, otherText.mText) && Objects.equals(mSpans, otherText.mSpans);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mText, mSpans);
+    }
+
+    /**
+     * Wraps a span to send it to the host.
+     */
+    public static class SpanWrapper {
+        @Keep
+        public final int start;
+        @Keep
+        public final int end;
+        @Keep
+        public final int flags;
+        @Keep
+        @Nullable
+        public final Object span;
+
+        static SpanWrapper wrap(Spanned spanned, Object span) {
+            return new SpanWrapper(spanned, span);
+        }
+
+        SpanWrapper(Spanned spanned, Object span) {
+            this.start = spanned.getSpanStart(span);
+            this.end = spanned.getSpanEnd(span);
+            this.flags = spanned.getSpanFlags(span);
+            this.span = span;
+        }
+
+        SpanWrapper() {
+            start = 0;
+            end = 0;
+            flags = 0;
+            span = null;
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof SpanWrapper)) {
+                return false;
+            }
+            SpanWrapper wrapper = (SpanWrapper) other;
+            return start == wrapper.start
+                    && end == wrapper.end
+                    && flags == wrapper.flags
+                    && Objects.equals(span, wrapper.span);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(start, end, flags, span);
+        }
+
+        @Override
+        public String toString() {
+            return "[" + span + ": " + start + ", " + end + ", flags: " + flags + "]";
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/DateTimeWithZone.java b/car/app/app/src/main/java/androidx/car/app/model/DateTimeWithZone.java
new file mode 100644
index 0000000..4b1416f
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/DateTimeWithZone.java
@@ -0,0 +1,218 @@
+/*
+ * 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.car.app.model;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.TextStyle;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.TimeZone;
+
+/**
+ * A time with an associated time zone information.
+ *
+ * <p>In order to avoid time zone databases being out of sync between the app and the host, this
+ * model avoids using <a href="https://www.iana.org/time-zones">IANA database</a> time zone IDs and
+ * instead relies on the app passing the time zone offset and its abbreviated name. Apps can use
+ * their time library of choice to retrieve the time zone information.
+ *
+ * <p>{@link #create(long, TimeZone)} and {@link #create(ZonedDateTime)} are provided for
+ * convenience if using {@code java.util} and {@code java.time} respectively. If using another
+ * library such as Joda time, {@link #create(long, int, String)} can be used.
+ */
+@SuppressWarnings("MissingSummary")
+public class DateTimeWithZone {
+    /** The maximum allowed offset for a time zone, in seconds. */
+    private static final long MAX_ZONE_OFFSET_SECONDS = 18 * HOURS.toSeconds(1);
+
+    @Keep
+    private final long mTimeSinceEpochMillis;
+    @Keep
+    private final int mZoneOffsetSeconds;
+    @Nullable
+    @Keep
+    private final String mZoneShortName;
+
+    /** Returns the number of milliseconds from the epoch of 1970-01-01T00:00:00Z. */
+    public long getTimeSinceEpochMillis() {
+        return mTimeSinceEpochMillis;
+    }
+
+    /** Returns the offset of the time zone from UTC. */
+    @SuppressLint("MethodNameUnits")
+    public int getZoneOffsetSeconds() {
+        return mZoneOffsetSeconds;
+    }
+
+    /**
+     * Returns the abbreviated name of the time zone, for example "PST" for Pacific Standard
+     * Time.
+     */
+    @Nullable
+    public String getZoneShortName() {
+        return mZoneShortName;
+    }
+
+    @Override
+    @NonNull
+    @RequiresApi(26)
+    // TODO(shiufai): consider removing the @RequiresApi annotation for a toString method.
+    @SuppressLint("UnsafeNewApiCall")
+    public String toString() {
+        return "[local: "
+                + LocalDateTime.ofEpochSecond(
+                mTimeSinceEpochMillis / 1000,
+                /* nanoOfSecond= */ 0,
+                ZoneOffset.ofTotalSeconds(mZoneOffsetSeconds))
+                + ", zone: "
+                + mZoneShortName
+                + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mTimeSinceEpochMillis, mZoneOffsetSeconds, mZoneShortName);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof DateTimeWithZone)) {
+            return false;
+        }
+        DateTimeWithZone otherDateTime = (DateTimeWithZone) other;
+
+        return mTimeSinceEpochMillis == otherDateTime.mTimeSinceEpochMillis
+                && mZoneOffsetSeconds == otherDateTime.mZoneOffsetSeconds
+                && Objects.equals(mZoneShortName, otherDateTime.mZoneShortName);
+    }
+
+    /**
+     * Returns an instance of a {@link DateTimeWithZone}.
+     *
+     * @param timeSinceEpochMillis The number of milliseconds from the epoch of
+     *                             1970-01-01T00:00:00Z.
+     * @param zoneOffsetSeconds    The offset of the time zone from UTC at the date specified by
+     *                             {@code timeInUtcMillis}. This offset must be in the range
+     *                             {@code -18:00} to {@code +18:00}, which corresponds to -64800
+     *                             to +64800.
+     * @param zoneShortName        The abbreviated name of the time zone, for example, "PST" for
+     *                             Pacific Standard Time. This string may be used to display to
+     *                             the user along with the date when needed, for example, if this
+     *                             time zone is different than the current system time zone.
+     * @throws IllegalArgumentException if {@code timeSinceEpochMillis} is a negative value.
+     * @throws IllegalArgumentException if {@code zoneOffsetSeconds} is no within the required
+     *                                  range.
+     * @throws NullPointerException     if {@code zoneShortName} is {@code null}.
+     * @throws IllegalArgumentException if {@code zoneShortName} is empty.
+     */
+    @NonNull
+    public static DateTimeWithZone create(
+            long timeSinceEpochMillis, int zoneOffsetSeconds, @NonNull String zoneShortName) {
+        if (timeSinceEpochMillis < 0) {
+            throw new IllegalArgumentException(
+                    "Time since epoch must be greater than or equal to zero");
+        }
+        if (Math.abs(zoneOffsetSeconds) > MAX_ZONE_OFFSET_SECONDS) {
+            throw new IllegalArgumentException("Zone offset not in valid range: -18:00 to +18:00");
+        }
+        if (requireNonNull(zoneShortName).isEmpty()) {
+            throw new IllegalArgumentException("The time zone short name can not be null or empty");
+        }
+        return new DateTimeWithZone(timeSinceEpochMillis, zoneOffsetSeconds, zoneShortName);
+    }
+
+    /**
+     * Returns an instance of a {@link DateTimeWithZone}.
+     *
+     * @param timeSinceEpochMillis The number of milliseconds from the epoch of
+     *                             1970-01-01T00:00:00Z.
+     * @param timeZone             The time zone at the date specified by {@code timeInUtcMillis}.
+     *                             The abbreviated
+     *                             name of this time zone, formatted using the default locale, may
+     *                             be displayed to the user
+     *                             when needed, for example, if this time zone is different than
+     *                             the current system time zone.
+     * @throws IllegalArgumentException if {@code timeSinceEpochMillis} is a negative value.
+     * @throws NullPointerException     if {@code timeZone} is {@code null}.
+     */
+    @NonNull
+    public static DateTimeWithZone create(long timeSinceEpochMillis, @NonNull TimeZone timeZone) {
+        if (timeSinceEpochMillis < 0) {
+            throw new IllegalArgumentException(
+                    "timeSinceEpochMillis must be greater than or equal to zero");
+        }
+        return create(
+                timeSinceEpochMillis,
+                (int) MILLISECONDS.toSeconds(
+                        requireNonNull(timeZone).getOffset(timeSinceEpochMillis)),
+                timeZone.getDisplayName(false, TimeZone.SHORT));
+    }
+
+    /**
+     * Returns an instance of a {@link DateTimeWithZone}.
+     *
+     * @param zonedDateTime The time with a time zone. The abbreviated name of this time zone,
+     *                      formatted using the default locale, may be displayed to the user when
+     *                      needed, for example,
+     *                      if this time zone is different than the current system time zone.
+     * @throws NullPointerException if {@code zonedDateTime} is {@code null}.
+     */
+    // TODO(shiufai): revisit wrapping this method in a container class (e.g. Api26Impl).
+    @SuppressLint("UnsafeNewApiCall")
+    @RequiresApi(26)
+    @NonNull
+    public static DateTimeWithZone create(@NonNull ZonedDateTime zonedDateTime) {
+        LocalDateTime localDateTime = requireNonNull(zonedDateTime).toLocalDateTime();
+        ZoneId zoneId = zonedDateTime.getZone();
+        ZoneOffset zoneOffset = zoneId.getRules().getOffset(localDateTime);
+        return create(
+                SECONDS.toMillis(localDateTime.toEpochSecond(zoneOffset)),
+                zoneOffset.getTotalSeconds(),
+                zoneId.getDisplayName(TextStyle.SHORT, Locale.getDefault()));
+    }
+
+    private DateTimeWithZone() {
+        mTimeSinceEpochMillis = 0;
+        mZoneOffsetSeconds = 0;
+        mZoneShortName = null;
+    }
+
+    private DateTimeWithZone(
+            long timeSinceEpochMillis, int zoneOffsetSeconds, @Nullable String timeZoneShortName) {
+        this.mTimeSinceEpochMillis = timeSinceEpochMillis;
+        this.mZoneOffsetSeconds = zoneOffsetSeconds;
+        this.mZoneShortName = timeZoneShortName;
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Distance.java b/car/app/app/src/main/java/androidx/car/app/model/Distance.java
new file mode 100644
index 0000000..2f1ea2d
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Distance.java
@@ -0,0 +1,215 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Locale;
+import java.util.Objects;
+
+/** Represents a distance value and how it should be displayed in the UI. */
+public final class Distance {
+    /**
+     * Possible units used to display {@link Distance}
+     *
+     * @hide
+     */
+    @IntDef({
+            UNIT_METERS,
+            UNIT_KILOMETERS,
+            UNIT_MILES,
+            UNIT_FEET,
+            UNIT_YARDS,
+            UNIT_KILOMETERS_P1,
+            UNIT_MILES_P1
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    // TODO(shiufai): investigate how to expose IntDefs if needed.
+    @RestrictTo(LIBRARY)
+    public @interface Unit {
+    }
+
+    /** Meter unit. */
+    @Unit
+    public static final int UNIT_METERS = 1;
+
+    /** Kilometer unit. */
+    @Unit
+    public static final int UNIT_KILOMETERS = 2;
+
+    /**
+     * Kilometer unit with the additional requirement that distances of this type be displayed
+     * with at least 1 digit of precision after the decimal point (for example, 2.0).
+     */
+    @Unit
+    public static final int UNIT_KILOMETERS_P1 = 3;
+
+    /** Miles unit. */
+    @Unit
+    public static final int UNIT_MILES = 4;
+
+    /**
+     * Mile unit with the additional requirement that distances of this type be displayed with at
+     * least 1 digit of precision after the decimal point (for example, 2.0).
+     */
+    @Unit
+    public static final int UNIT_MILES_P1 = 5;
+
+    /** Feet unit. */
+    @Unit
+    public static final int UNIT_FEET = 6;
+
+    /** Yards unit. */
+    @Unit
+    public static final int UNIT_YARDS = 7;
+
+    @Keep
+    private final double mDisplayDistance;
+    @Keep
+    @Unit
+    private final int mDisplayUnit;
+
+    /**
+     * Constructs a new instance of a {@link Distance}.
+     *
+     * <p>Units with precision requirements, {@link #UNIT_KILOMETERS_P1} and {@link #UNIT_MILES_P1},
+     * will always show one decimal digit. All other units will show a decimal digit if needed but
+     * will not if the distance is a whole number.
+     *
+     * <h4>Examples</h4>
+     *
+     * A display distance of 1.0 with a display unit of {@link #UNIT_KILOMETERS} will display "1
+     * km", whereas if the display unit is {@link #UNIT_KILOMETERS_P1} it will display "1.0 km".
+     * Note the "km" part of the string in this example depends on the locale the host is
+     * configured with.
+     *
+     * <p>A display distance of 1.46 however will display "1.4 km" for both {@link #UNIT_KILOMETERS}
+     * and {@link #UNIT_KILOMETERS} display units.
+     *
+     * <p>{@link #UNIT_KILOMETERS_P1} and {@link #UNIT_MILES_P1} can be used to provide consistent
+     * digit placement for a sequence of distances. For example, as the user is driving and the next
+     * turn distance changes, using {@link #UNIT_KILOMETERS_P1} will produce: "2.5 km", "2.0 km",
+     * "1.5 km", "1.0 km", and so on.
+     *
+     * @param displayDistance the distance to display, in the units specified in {@code
+     *                        displayUnit}. See {@link #getDisplayDistance()}.
+     * @param displayUnit     the unit of distance to use when displaying the value in {@code
+     *                        displayUnit}. This should be one of the {@code UNIT_*} static
+     *                        constants defined in this class. See {@link #getDisplayUnit()}.
+     * @throws IllegalArgumentException if {@code displayDistance} is negative.
+     */
+    @NonNull
+    public static Distance create(double displayDistance, @Unit int displayUnit) {
+        if (displayDistance < 0) {
+            throw new IllegalArgumentException("displayDistance must be a positive value");
+        }
+        return new Distance(displayDistance, displayUnit);
+    }
+
+    /**
+     * Returns the distance measured in the unit indicated at {@link #getDisplayUnit()}.
+     *
+     * <p>This distance is for display purposes only and it might be a rounded representation of the
+     * actual distance. For example, a distance of 1000 meters could be shown in the following ways:
+     *
+     * <ul>
+     *   <li>Display unit of {@link #UNIT_METERS} and distance of 1000, resulting in a display of
+     *       "1000 m".
+     *   <li>Display unit of {@link #UNIT_KILOMETERS} and distance of 1, resulting in a
+     *       display of "1 km".
+     *   <li>Display unit of {@link #UNIT_KILOMETERS_P1} and distance of 1, resulting in a
+     *       display of "1.0 km".
+     * </ul>
+     */
+    public double getDisplayDistance() {
+        return mDisplayDistance;
+    }
+
+    /**
+     * Returns the unit that should be used to display the distance value, adjusted to the current
+     * user's locale and location. This should match the unit used in {@link #getDisplayDistance()}.
+     */
+    @Unit
+    public int getDisplayUnit() {
+        return mDisplayUnit;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return String.format(Locale.US, "%.04f%s", mDisplayDistance, unitToString(mDisplayUnit));
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mDisplayDistance, mDisplayUnit);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof Distance)) {
+            return false;
+        }
+        Distance otherDistance = (Distance) other;
+
+        return mDisplayUnit == otherDistance.mDisplayUnit
+                && mDisplayDistance == otherDistance.mDisplayDistance;
+    }
+
+    private Distance(double displayDistance, @Unit int displayUnit) {
+        this.mDisplayDistance = displayDistance;
+        this.mDisplayUnit = displayUnit;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private Distance() {
+        mDisplayDistance = 0.0d;
+        mDisplayUnit = UNIT_METERS;
+    }
+
+    private static String unitToString(@Unit int displayUnit) {
+        switch (displayUnit) {
+            case UNIT_FEET:
+                return "ft";
+            case UNIT_KILOMETERS:
+                return "km";
+            case UNIT_KILOMETERS_P1:
+                return "km_p1";
+            case UNIT_METERS:
+                return "m";
+            case UNIT_MILES:
+                return "mi";
+            case UNIT_MILES_P1:
+                return "mi_p1";
+            case UNIT_YARDS:
+                return "yd";
+            default:
+                return "?";
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/DistanceSpan.java b/car/app/app/src/main/java/androidx/car/app/model/DistanceSpan.java
new file mode 100644
index 0000000..9fe03a8
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/DistanceSpan.java
@@ -0,0 +1,114 @@
+/*
+ * 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.car.app.model;
+
+import static java.util.Objects.requireNonNull;
+
+import android.text.TextPaint;
+import android.text.style.CharacterStyle;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * A span that replaces the text it is attached to with the string representation of a {@link
+ * Distance} instance.
+ *
+ * <p>The {@link Distance} instance will be displayed by the host in a localized format, so that it
+ * will be consistent with the rest of the user interface where distance information are displayed.
+ *
+ * <p>For example, the following code creates a string that shows the distance as the first text in
+ * the string before the interpunct:
+ *
+ * <pre>{@code
+ * String interpunct = "\u00b7";
+ * SpannableString string = new SpannableString("  " + interpunct + " Point-of-Interest 1");
+ * string.setSpan(
+ *   DistanceSpan.create(
+ *     Distance.create(1000, "1.0", UNIT_KILOMETERS)), 0, 1, SPAN_INCLUSIVE_INCLUSIVE);
+ * }</pre>
+ *
+ * <p>The span flags (e.g. SPAN_EXCLUSIVE_EXCLUSIVE) will be ignored.
+ *
+ * <p>This span will be ignored if it overlaps with any span that replaces text, such as another
+ * {@link DistanceSpan}, {@link DurationSpan}, or {@link CarIconSpan}. However, it is possible to
+ * apply styling to the text, such as changing colors:
+ *
+ * <pre>{@code
+ * String interpunct = "\u00b7";
+ * SpannableString string = new SpannableString("  " + interpunct + " Point-of-Interest 1");
+ * string.setSpan(
+ *   DistanceSpan.create(
+ *     Distance.create(1000, "1.0", UNIT_KILOMETERS)), 0, 1, SPAN_INCLUSIVE_INCLUSIVE);
+ * string.setSpan(ForegroundCarColorSpan.create(CarColor.BLUE), 0, 1, SPAN_EXCLUSIVE_EXCLUSIVE);
+ * }</pre>
+ */
+public class DistanceSpan extends CharacterStyle {
+    @Nullable
+    @Keep
+    private final Distance mDistance;
+
+    /** Creates a {@link DistanceSpan} from a {@link CarIcon}. */
+    @NonNull
+    public static DistanceSpan create(@NonNull Distance distance) {
+        return new DistanceSpan(requireNonNull(distance));
+    }
+
+    private DistanceSpan(Distance distance) {
+        this.mDistance = distance;
+    }
+
+    private DistanceSpan() {
+        mDistance = null;
+    }
+
+    @NonNull
+    public Distance getDistance() {
+        return requireNonNull(mDistance);
+    }
+
+    @Override
+    public void updateDrawState(@Nullable TextPaint paint) {
+        // Not relevant.
+    }
+
+    @Override
+    public String toString() {
+        return "[distance: " + mDistance + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mDistance);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof DistanceSpan)) {
+            return false;
+        }
+        DistanceSpan otherSpan = (DistanceSpan) other;
+
+        return Objects.equals(mDistance, otherSpan.mDistance);
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/DurationSpan.java b/car/app/app/src/main/java/androidx/car/app/model/DurationSpan.java
new file mode 100644
index 0000000..05e58a9
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/DurationSpan.java
@@ -0,0 +1,118 @@
+/*
+ * 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.car.app.model;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+import android.text.TextPaint;
+import android.text.style.CharacterStyle;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.time.Duration;
+
+/**
+ * A span that replaces the text it is attached to with a localized duration string.
+ *
+ * <p>For example, the following code creates a string that shows the duration as the first text in
+ * the string before the interpunct:
+ *
+ * <pre>{@code
+ * String interpunct = "\u00b7";
+ * SpannableString string = new SpannableString("  " + interpunct + " Point-of-Interest 1");
+ * string.setSpan(DurationSpan.create(300), 0, 1, SPAN_INCLUSIVE_INCLUSIVE);
+ * }</pre>
+ *
+ * <p>The span flags (e.g. SPAN_EXCLUSIVE_EXCLUSIVE) will be ignored.
+ *
+ * <p>This span will be ignored if it overlaps with any span that replaces text, such as another
+ * {@link DistanceSpan}, {@link DurationSpan}, or {@link CarIconSpan}. However, it is possible to *
+ * apply styling to the text, such as changing colors:
+ *
+ * <pre>{@code
+ * String interpunct = "\u00b7";
+ * SpannableString string = new SpannableString("  " + interpunct + " Point-of-Interest 1");
+ * string.setSpan(DurationSpan.create(300), 0, 1, SPAN_INCLUSIVE_INCLUSIVE);
+ * string.setSpan(ForegroundCarColorSpan.create(CarColor.BLUE), 0, 1, SPAN_EXCLUSIVE_EXCLUSIVE);
+ * }</pre>
+ */
+public class DurationSpan extends CharacterStyle {
+    @Keep
+    private final long mDurationSeconds;
+
+    /** Creates a {@link DurationSpan} with the given duration. */
+    @NonNull
+    public static DurationSpan create(long durationSeconds) {
+        return new DurationSpan(durationSeconds);
+    }
+
+    /** Creates a {@link DurationSpan} with the given duration. */
+    @NonNull
+    @RequiresApi(26)
+    // TODO(shiufai): revisit wrapping this method in a container class (e.g. Api26Impl).
+    @SuppressLint("UnsafeNewApiCall")
+    public static DurationSpan create(@NonNull Duration duration) {
+        return new DurationSpan(requireNonNull(duration).getSeconds());
+    }
+
+    private DurationSpan(long durationSeconds) {
+        this.mDurationSeconds = durationSeconds;
+    }
+
+    private DurationSpan() {
+        mDurationSeconds = 0;
+    }
+
+    @SuppressLint("MethodNameUnits")
+    public long getDurationSeconds() {
+        return mDurationSeconds;
+    }
+
+    @Override
+    public void updateDrawState(@Nullable TextPaint paint) {
+        // Not relevant.
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[seconds: " + mDurationSeconds + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        // Equivalent implementation as Long.hashcode() but avoids the boxing.
+        return (int) (mDurationSeconds ^ (mDurationSeconds >>> 32));
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof DurationSpan)) {
+            return false;
+        }
+        DurationSpan otherSpan = (DurationSpan) other;
+
+        return mDurationSeconds == otherSpan.mDurationSeconds;
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ForegroundCarColorSpan.java b/car/app/app/src/main/java/androidx/car/app/model/ForegroundCarColorSpan.java
new file mode 100644
index 0000000..b9fdfb3
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ForegroundCarColorSpan.java
@@ -0,0 +1,122 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.text.TextPaint;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.constraints.CarColorConstraints;
+
+import java.util.Objects;
+
+/**
+ * A span that changes the color of the text to which the span is attached.
+ *
+ * <p>For example, to set a green text color to a span of a string:
+ *
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with a foreground color span");
+ * string.setSpan(ForegroundCarColorSpan.create(CarColor.GREEN),
+ *     12, 28, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE));
+ * }</pre>
+ *
+ * <p>The host may ignore the color specified in the {@link ForegroundCarColorSpan} and instead use
+ * a default color unless support for {@link ForegroundCarColorSpan} is explicitly documented in the
+ * API that takes the string. The host may use a default color if the color in the span does not
+ * pass the contrast requirements.
+ *
+ * @see CarColor
+ * @see ForegroundColorSpan
+ */
+public class ForegroundCarColorSpan extends CharacterStyle {
+    @Keep
+    private final CarColor mCarColor;
+
+    @NonNull
+    public CarColor getColor() {
+        return mCarColor;
+    }
+
+    /**
+     * Creates a {@link ForegroundColorSpan} from a {@link CarColor}.
+     *
+     * <p>Custom colors created with {@link CarColor#createCustom} are not supported in text spans.
+     *
+     * @throws IllegalArgumentException if {@code carColor} contains a custom color.
+     * @throws NullPointerException     if {@code carColor} is {@code null}.
+     */
+    @NonNull
+    public static ForegroundCarColorSpan create(@NonNull CarColor carColor) {
+        CarColorConstraints.STANDARD_ONLY.validateOrThrow(carColor);
+        return new ForegroundCarColorSpan(requireNonNull(carColor));
+    }
+
+    /** @hide */
+    @RestrictTo(LIBRARY)
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    @NonNull
+    public static ForegroundCarColorSpan createForTesting(@NonNull CarColor carColor) {
+        return new ForegroundCarColorSpan(carColor);
+    }
+
+    @Override
+    public void updateDrawState(@NonNull TextPaint paint) {
+        // Not relevant.
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[color: " + mCarColor + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mCarColor);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof ForegroundCarColorSpan)) {
+            return false;
+        }
+        ForegroundCarColorSpan otherSpan = (ForegroundCarColorSpan) other;
+
+        return Objects.equals(mCarColor, otherSpan.mCarColor);
+    }
+
+    private ForegroundCarColorSpan(CarColor carColor) {
+        this.mCarColor = carColor;
+    }
+
+    private ForegroundCarColorSpan() {
+        mCarColor = CarColor.DEFAULT;
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
new file mode 100644
index 0000000..535d90c
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
@@ -0,0 +1,330 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.constraints.CarIconConstraints;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represents a grid item with an image and an optional title.
+ */
+// TODO(shiufai): Support toggle state in a grid item.
+// TODO(shiufai): Make grid item browsable.
+public class GridItem implements Item {
+    /**
+     * The type of images supported within grid items.
+     *
+     * @hide
+     */
+    // TODO(shiufai): investigate how to expose IntDefs if needed.
+    @RestrictTo(LIBRARY)
+    @IntDef(value = {IMAGE_TYPE_ICON, IMAGE_TYPE_LARGE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface GridItemImageType {
+    }
+
+    /**
+     * Represents an icon to be displayed in the grid item.
+     *
+     * <p>If necessary, icons will be scaled down to fit within a 44 x 44 dp bounding box,
+     * preserving
+     * their aspect ratios.
+     *
+     * <p>A tint color is expected to be provided via {@link CarIcon.Builder#setTint}. Otherwise, a
+     * default tint color as determined by the host will be applied.
+     */
+    public static final int IMAGE_TYPE_ICON = (1 << 0);
+
+    /**
+     * Represents a large image to be displayed in the grid item.
+     *
+     * <p>If necessary, these images will be scaled down to fit within a 80 x 80 dp bounding box,
+     * preserving their aspect ratio.
+     */
+    public static final int IMAGE_TYPE_LARGE = (1 << 1);
+
+    @Keep
+    @Nullable
+    private final CarText mTitle;
+    @Keep
+    @Nullable
+    private final CarText mText;
+    @Keep
+    @Nullable
+    private final CarIcon mImage;
+    @Keep
+    @Nullable
+    private final Toggle mToggle;
+    @Keep
+    @GridItemImageType
+    private final int mImageType;
+    @Keep
+    @Nullable
+    private final OnClickListenerWrapper mOnClickListener;
+
+    /** Constructs a new builder of {@link GridItem}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** Returns the title of the grid item. */
+    @Nullable
+    public CarText getTitle() {
+        return mTitle;
+    }
+
+    /** Returns the list of text below the title. */
+    @Nullable
+    public CarText getText() {
+        return mText;
+    }
+
+    /** Returns the image of the grid item. */
+    @NonNull
+    public CarIcon getImage() {
+        return requireNonNull(mImage);
+    }
+
+    /** Returns the image type of the grid item. */
+    @GridItemImageType
+    public int getImageType() {
+        return mImageType;
+    }
+
+    /**
+     * Returns the {@link Toggle} in the grid item or {@code null} if the grid item does not
+     * contain a toggle.
+     */
+    @Nullable
+    public Toggle getToggle() {
+        return mToggle;
+    }
+
+    /**
+     * Returns the {@link OnClickListener} to be called back when the grid item is clicked, or
+     * {@code null} if the grid item is non-clickable.
+     */
+    @Nullable
+    public OnClickListenerWrapper getOnClickListener() {
+        return mOnClickListener;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[title: "
+                + CarText.toShortString(mTitle)
+                + ", text: "
+                + CarText.toShortString(mText)
+                + ", image: "
+                + mImage
+                + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mTitle, mImage, mImageType, mToggle, mOnClickListener == null);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof GridItem)) {
+            return false;
+        }
+        GridItem otherGridItem = (GridItem) other;
+
+        return Objects.equals(mTitle, otherGridItem.mTitle)
+                && Objects.equals(mText, otherGridItem.mText)
+                && Objects.equals(mImage, otherGridItem.mImage)
+                && Objects.equals(mToggle, otherGridItem.mToggle)
+                && Objects.equals(mOnClickListener == null, otherGridItem.mOnClickListener == null)
+                && mImageType == otherGridItem.mImageType;
+    }
+
+    private GridItem(Builder builder) {
+        mTitle = builder.mTitle;
+        mText = builder.mText;
+        mImage = builder.mImage;
+        mImageType = builder.mImageType;
+        mToggle = builder.mToggle;
+        mOnClickListener = builder.mOnClickListener;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private GridItem() {
+        mTitle = null;
+        mText = null;
+        mImage = null;
+        mImageType = IMAGE_TYPE_LARGE;
+        mToggle = null;
+        mOnClickListener = null;
+    }
+
+    /** A builder of {@link GridItem}. */
+    public static final class Builder {
+        @Nullable
+        private CarText mTitle;
+        @Nullable
+        private CarText mText;
+        @Nullable
+        private CarIcon mImage;
+        @GridItemImageType
+        private int mImageType = IMAGE_TYPE_LARGE;
+        @Nullable
+        private Toggle mToggle;
+        @Nullable
+        private OnClickListenerWrapper mOnClickListener;
+
+        /** Sets the title of the grid item, or {@code null} to not show the title. */
+        @NonNull
+        public Builder setTitle(@Nullable CharSequence title) {
+            this.mTitle = title == null ? null : CarText.create(title);
+            return this;
+        }
+
+        /**
+         * Sets the text string to the grid item that is displayed below the title, or {@code
+         * null} to not show any text below the title.
+         *
+         * <h2>Text Wrapping</h2>
+         *
+         * The string added with {@link #setText} is truncated at the end to fit in a single line
+         * below
+         * the title.
+         */
+        @NonNull
+        public Builder setText(@Nullable CharSequence text) {
+            this.mText = text == null ? null : CarText.create(text);
+            return this;
+        }
+
+        /**
+         * Sets an image to show in the grid item with the default size {@link #IMAGE_TYPE_LARGE}.
+         *
+         * @see #setImage(CarIcon, int)
+         */
+        @NonNull
+        public Builder setImage(@NonNull CarIcon image) {
+            return setImage(image, IMAGE_TYPE_LARGE);
+        }
+
+        /**
+         * Sets an image to show in the grid item with the given {@code imageType}.
+         *
+         * <p>For a custom {@link CarIcon}, its {@link androidx.core.graphics.drawable.IconCompat}
+         * instance can be of {@link androidx.core.graphics.drawable.IconCompat#TYPE_BITMAP},
+         * {@link androidx.core.graphics.drawable.IconCompat#TYPE_RESOURCE}, or
+         * {@link androidx.core.graphics.drawable.IconCompat#TYPE_URI}.
+         *
+         * <h4>Image Sizing Guidance</h4>
+         *
+         * <p>If the input image's size exceeds the sizing requirements for the given image type in
+         * either one of the dimensions, it will be scaled down to be centered inside the
+         * bounding box while preserving the aspect ratio.
+         *
+         * <p>See {@link CarIcon} for more details related to providing icon and image resources
+         * that work with different car screen pixel densities.
+         *
+         * @param image     the {@link CarIcon} to display.
+         * @param imageType one of {@link #IMAGE_TYPE_ICON} or {@link #IMAGE_TYPE_LARGE}.
+         */
+        @NonNull
+        public Builder setImage(@NonNull CarIcon image, @GridItemImageType int imageType) {
+            CarIconConstraints.UNCONSTRAINED.validateOrThrow(image);
+            this.mImage = image;
+            this.mImageType = imageType;
+            return this;
+        }
+
+        /**
+         * Sets a {@link Toggle} for the grid item, or {@code null} to not have any toggle states
+         * in the grid item. If set, this grid item acts as a toggle.
+         *
+         * <p>If the grid item has a {@link Toggle}, then no {@link OnClickListener} can be added
+         * to it.
+         */
+        @NonNull
+        public Builder setToggle(@Nullable Toggle toggle) {
+            this.mToggle = toggle;
+            return this;
+        }
+
+        /**
+         * Sets the {@link OnClickListener} to be called back when the grid item is clicked, or
+         * {@code null} to make the grid item non-clickable.
+         */
+        @NonNull
+        @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+        public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
+            if ( null) {
+                this.mOnClickListener = null;
+            } else {
+                this.mOnClickListener = OnClickListenerWrapper.create(onClickListener);
+            }
+            return this;
+        }
+
+        /**
+         * Constructs the {@link GridItem} defined by this builder.
+         *
+         * @throws IllegalStateException if the grid item's image is not set.
+         * @throws IllegalStateException if the grid item doesn't have a title but the text is set.
+         * @throws IllegalStateException if the grid item has both a {@link OnClickListener} and a
+         *                               {@link Toggle}.
+         */
+        @NonNull
+        public GridItem build() {
+            if (mImage == null) {
+                throw new IllegalStateException("An image must be set on the grid item");
+            }
+
+            if (mTitle == null && mText != null) {
+                throw new IllegalStateException(
+                        "If a grid item doesn't have a title, it must not have a text set");
+            }
+
+            if (mToggle != null && mOnClickListener != null) {
+                throw new IllegalStateException(
+                        "If a grid item contains a toggle, it must not have a onClickListener set"
+                                + " and vice versa");
+            }
+
+            return new GridItem(this);
+        }
+
+        private Builder() {
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/GridTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/GridTemplate.java
new file mode 100644
index 0000000..a3a4ddf
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/GridTemplate.java
@@ -0,0 +1,341 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.constraints.CarIconConstraints;
+import androidx.car.app.utils.Logger;
+
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * A template representing a grid of items.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in
+ * {@link androidx.car.app.Screen#getTemplate()}, this template is considered a refresh of a
+ * previous one if:
+ *
+ * <ul>
+ *   <li>The template title has not changed, and
+ *   <li>The previous template is in a loading state (see {@link Builder#setLoading}, or the
+ *       number of grid items and the string contents (title, texts) of each grid item have not
+ *       changed.
+ *   <li>For grid items that contain a {@link Toggle}, updates to the title, text and image are also
+ *       allowed if the toggle state has changed between the previous and new templates.
+ * </ul>
+ */
+public final class GridTemplate implements Template {
+    @Keep
+    private final boolean mIsLoading;
+    @Keep
+    @Nullable
+    private final CarText mTitle;
+    @Keep
+    @Nullable
+    private final Action mHeaderAction;
+    @Keep
+    @Nullable
+    private final ItemList mSingleList;
+    @Keep
+    @Nullable
+    private final ActionStrip mActionStrip;
+    @Keep
+    @Nullable
+    private final CarIcon mBackgroundImage;
+
+    /** Constructs a new builder of {@link GridTemplate}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public boolean isLoading() {
+        return mIsLoading;
+    }
+
+    @Nullable
+    public CarText getTitle() {
+        return mTitle;
+    }
+
+    @Nullable
+    public Action getHeaderAction() {
+        return mHeaderAction;
+    }
+
+    @Nullable
+    public ItemList getSingleList() {
+        return mSingleList;
+    }
+
+    @Nullable
+    public ActionStrip getActionStrip() {
+        return mActionStrip;
+    }
+
+    @Nullable
+    public CarIcon getBackgroundImage() {
+        return mBackgroundImage;
+    }
+
+    @Override
+    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+        requireNonNull(oldTemplate);
+
+        if (oldTemplate.getClass() != this.getClass()) {
+            return false;
+        }
+
+        GridTemplate old = (GridTemplate) oldTemplate;
+
+        if (!Objects.equals(old.getTitle(), getTitle())) {
+            return false;
+        }
+
+        if (old.mIsLoading) {
+            // Transition from a previous loading state is allowed.
+            return true;
+        } else if (mIsLoading) {
+            // Transition to a loading state is disallowed.
+            return false;
+        }
+
+        if (mSingleList != null && old.mSingleList != null) {
+            return mSingleList.isRefresh(old.mSingleList, logger);
+        }
+
+        return true;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "GridTemplate";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mIsLoading, mTitle, mHeaderAction, mSingleList, mActionStrip,
+                mBackgroundImage);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof GridTemplate)) {
+            return false;
+        }
+        GridTemplate otherTemplate = (GridTemplate) other;
+
+        return mIsLoading == otherTemplate.mIsLoading
+                && Objects.equals(mTitle, otherTemplate.mTitle)
+                && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+                && Objects.equals(mSingleList, otherTemplate.mSingleList)
+                && Objects.equals(mActionStrip, otherTemplate.mActionStrip)
+                && Objects.equals(mBackgroundImage, otherTemplate.mBackgroundImage);
+    }
+
+    private GridTemplate(Builder builder) {
+        mIsLoading = builder.mIsLoading;
+        mTitle = builder.mTitle;
+        mHeaderAction = builder.mHeaderAction;
+        mSingleList = builder.mSingleList;
+        mActionStrip = builder.mActionStrip;
+        mBackgroundImage = builder.mBackgroundImage;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private GridTemplate() {
+        mIsLoading = false;
+        mTitle = null;
+        mHeaderAction = null;
+        mSingleList = null;
+        mActionStrip = null;
+        mBackgroundImage = null;
+    }
+
+    /** A builder of {@link GridTemplate}. */
+    public static final class Builder {
+        private boolean mIsLoading;
+        @Nullable
+        private ItemList mSingleList;
+        @Nullable
+        private CarText mTitle;
+        @Nullable
+        private Action mHeaderAction;
+        @Nullable
+        private ActionStrip mActionStrip;
+
+        /** For internal, host-side use only. */
+        @Nullable
+        private CarIcon mBackgroundImage;
+
+        /**
+         * Sets whether the template is in a loading state.
+         *
+         * <p>If set to {@code true}, the UI shows a loading indicator where the grid content
+         * would be
+         * otherwise. The caller is expected to call {@link androidx.car.app.Screen#invalidate()}
+         * and send the new template content to the host once the data is ready. If set to {@code
+         * false}, the UI shows the {@link ItemList} contents added via {@link #setSingleList}.
+         */
+        @NonNull
+        public Builder setLoading(boolean isLoading) {
+            this.mIsLoading = isLoading;
+            return this;
+        }
+
+        /**
+         * Sets the {@link Action} that will be displayed in the header of the template, or
+         * {@code null}
+         * to not display an action.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template only supports either either one of {@link Action#APP_ICON} and {@link
+         * Action#BACK} as a header {@link Action}.
+         *
+         * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setHeaderAction(@Nullable Action headerAction) {
+            ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+                    headerAction == null ? Collections.emptyList()
+                            : Collections.singletonList(headerAction));
+            this.mHeaderAction = headerAction;
+            return this;
+        }
+
+        /** Sets the {@link CharSequence} to show as title, or {@code null} to not show a title. */
+        @NonNull
+        public Builder setTitle(@Nullable CharSequence title) {
+            this.mTitle = title == null ? null : CarText.create(title);
+            return this;
+        }
+
+        /**
+         * Sets a single {@link ItemList} to show in the template.
+         *
+         * @throws NullPointerException if {@code list} is null.
+         */
+        @NonNull
+        public Builder setSingleList(@NonNull ItemList list) {
+            mSingleList = requireNonNull(list);
+            return this;
+        }
+
+        /** Resets the list that was added via {@link #setSingleList}. */
+        @NonNull
+        public Builder clearAllLists() {
+            mSingleList = null;
+            return this;
+        }
+
+        /**
+         * Sets the {@link ActionStrip} for this template, or {@code null} to not display an {@link
+         * ActionStrip}.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
+         * {@link Action}s, one of them can contain a title as set via
+         * {@link Action.Builder#setTitle}.
+         * Otherwise, only {@link Action}s with icons are allowed.
+         *
+         * @throws IllegalArgumentException if {@code actionStrip} does not meet the requirements.
+         */
+        @NonNull
+        public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+            ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+                    actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+            this.mActionStrip = actionStrip;
+            return this;
+        }
+
+        /**
+         * Sets a {@link CarIcon} to be shown as background of the template.
+         *
+         * <p>For internal, host-side use only.
+         */
+        @NonNull
+        public Builder setBackgroundImage(@Nullable CarIcon backgroundImage) {
+            CarIconConstraints.UNCONSTRAINED.validateOrThrow(backgroundImage);
+            this.mBackgroundImage = backgroundImage;
+            return this;
+        }
+
+        /**
+         * Constructs the template defined by this builder.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 6 {@link GridItem}s total in the {@link ItemList}(s). The host
+         * will ignore any items over that limit.
+         *
+         * <p>Either a header {@link Action} or title must be set on the template.
+         *
+         * @throws IllegalStateException    if the template is in a loading state but there are
+         *                                  lists
+         *                                  added, or vice versa.
+         * @throws IllegalArgumentException if the added {@link ItemList} does not meet the
+         *                                  template's
+         *                                  requirements.
+         * @throws IllegalStateException    if the template does not have either a title or header
+         *                                  {@link
+         *                                  Action} set.
+         */
+        @NonNull
+        public GridTemplate build() {
+            boolean hasList = mSingleList != null;
+            if (mIsLoading == hasList) {
+                throw new IllegalStateException(
+                        "Template is in a loading state but lists are added, or vice versa");
+            }
+
+            if (mSingleList != null) {
+                for (Object gridItemObject : mSingleList.getItems()) {
+                    if (!(gridItemObject instanceof GridItem)) {
+                        throw new IllegalArgumentException(
+                                "All the items in grid template's item list must be grid items");
+                    }
+                }
+            }
+
+            if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+                throw new IllegalStateException("Either the title or header action must be set");
+            }
+
+            return new GridTemplate(this);
+        }
+
+        private Builder() {
+        }
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java b/car/app/app/src/main/java/androidx/car/app/model/Item.java
similarity index 74%
copy from camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java
copy to car/app/app/src/main/java/androidx/car/app/model/Item.java
index d6b6436..6e50daf 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Item.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -14,10 +14,8 @@
  * limitations under the License.
  */
 
-/**
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-package androidx.camera.core.impl.quirk;
+package androidx.car.app.model;
 
-import androidx.annotation.RestrictTo;
+/** Interface implemented by models that can be added to an {@link ItemList}. */
+public interface Item {
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ItemList.java b/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
new file mode 100644
index 0000000..92649b7
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
@@ -0,0 +1,436 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.IOnItemVisibilityChangedListener;
+import androidx.car.app.IOnSelectedListener;
+import androidx.car.app.utils.Logger;
+import androidx.car.app.utils.RemoteUtils;
+import androidx.car.app.utils.ValidationUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a list of {@link Item} instances. {@link ItemList} instances are used by templates
+ * that contain lists of models, such as for example, the list of {@link Row}s in a {@link
+ * ListTemplate}.
+ */
+public final class ItemList {
+    /**
+     * A listener for handling selection events for lists with selectable items.
+     *
+     * @see Builder#setSelectable(OnSelectedListener)
+     */
+    public interface OnSelectedListener {
+        /**
+         * Notifies that an item was selected.
+         *
+         * <p>This event is called even if the selection did not change, for example, if the user
+         * selected an already selected item.
+         *
+         * @param selectedIndex the index of the newly selected item.
+         */
+        void onSelected(int selectedIndex);
+    }
+
+    /** A listener for handling item visibility changes. */
+    public interface OnItemVisibilityChangedListener {
+        /**
+         * Notifies that the items in the list within the specified indices have become visible.
+         *
+         * <p>The start index is inclusive, and the end index is exclusive. For example, if only the
+         * first item in a list is visible, the start and end indices would be 0 and 1,
+         * respectively. If
+         * no items are visible, the indices will be set to -1.
+         *
+         * @param startIndex the index of the first item that is visible.
+         * @param endIndex   the index of the first item that is not visible after the visible
+         *                   range.
+         */
+        void onItemVisibilityChanged(int startIndex, int endIndex);
+    }
+
+    @Keep
+    private final int mSelectedIndex;
+    @Keep
+    private final List<Object> mItems;
+    @Keep
+    @Nullable
+    private final IOnSelectedListener mOnSelectedListener;
+    @Keep
+    @Nullable
+    private final IOnItemVisibilityChangedListener mItemVisibilityChangedListener;
+    @Keep
+    @Nullable
+    private final CarText mNoItemsMessage;
+
+    /** Constructs a new builder of {@link ItemList}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** Returns the index of the selected item of the list. */
+    public int getSelectedIndex() {
+        return mSelectedIndex;
+    }
+
+    /**
+     * Returns the {@link OnSelectedListener} to be called when when an item is selected by the
+     * user, or {@code null} is the list is non-selectable.
+     *
+     * @hide
+     */
+    // TODO(shiufai): re-surface this API with a wrapper around the AIDL class.
+    @RestrictTo(LIBRARY)
+    @Nullable
+    public IOnSelectedListener getOnSelectedListener() {
+        return mOnSelectedListener;
+    }
+
+    /** Returns the text to be displayed if the list is empty. */
+    @Nullable
+    public CarText getNoItemsMessage() {
+        return mNoItemsMessage;
+    }
+
+    /**
+     * Returns the {@link OnItemVisibilityChangedListener} to be called when the visible items in
+     * the list changes.
+     *
+     * @hide
+     */
+    // TODO(shiufai): re-surface this API with a wrapper around the AIDL class.
+    @RestrictTo(LIBRARY)
+    @Nullable
+    public IOnItemVisibilityChangedListener getOnItemsVisibilityChangeListener() {
+        return mItemVisibilityChangedListener;
+    }
+
+    /** Returns the list of items in this {@link ItemList}. */
+    @NonNull
+    public List<Object> getItems() {
+        return mItems;
+    }
+
+    /**
+     * Returns {@code true} if this {@link ItemList} instance is determined to be a refresh of the
+     * given list, or {@code false} otherwise.
+     *
+     * <p>A list is considered a refresh if:
+     *
+     * <ul>
+     *   <li>The other list is in a loading state, or
+     *   <li>The item size and string contents of the two lists are the same. For rows that
+     *   contain a
+     *       {@link Toggle}, the string contents can be updated if the toggle state has changed
+     *       between the previous and new rows. For grid items that contain a {@link Toggle}, string
+     *       contents and images can be updated if the toggle state has changed.
+     * </ul>
+     */
+    public boolean isRefresh(@Nullable ItemList other, @NonNull Logger logger) {
+        if (other == null) {
+            return false;
+        }
+
+        return ValidationUtils.itemsHaveSameContent(
+                other.getItems(), other.getSelectedIndex(), getItems(), getSelectedIndex(), logger);
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[ items: "
+                + (mItems != null ? mItems.toString() : null)
+                + ", selected: "
+                + mSelectedIndex
+                + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mSelectedIndex,
+                mItems,
+                mOnSelectedListener == null,
+                mItemVisibilityChangedListener == null,
+                mNoItemsMessage);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof ItemList)) {
+            return false;
+        }
+        ItemList otherList = (ItemList) other;
+
+        // For listeners only check if they are either both null, or both set.
+        return mSelectedIndex == otherList.mSelectedIndex
+                && Objects.equals(mItems, otherList.mItems)
+                && Objects.equals(mOnSelectedListener == null,
+                otherList.mOnSelectedListener == null)
+                && Objects.equals(
+                mItemVisibilityChangedListener == null,
+                otherList.mItemVisibilityChangedListener == null)
+                && Objects.equals(mNoItemsMessage, otherList.mNoItemsMessage);
+    }
+
+    private ItemList(Builder builder) {
+        mSelectedIndex = builder.mSelectedIndex;
+        mItems = new ArrayList<>(builder.mItems);
+        mNoItemsMessage = builder.mNoItemsMessage;
+        mOnSelectedListener = builder.mOnSelectedListener;
+        mItemVisibilityChangedListener = builder.mOnItemVisibilityChangedListener;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private ItemList() {
+        mSelectedIndex = 0;
+        mItems = Collections.emptyList();
+        mNoItemsMessage = null;
+        mOnSelectedListener = null;
+        mItemVisibilityChangedListener = null;
+    }
+
+    /** A builder of {@link ItemList}. */
+    public static final class Builder {
+        private final List<Object> mItems = new ArrayList<>();
+        private int mSelectedIndex;
+        @Nullable
+        private IOnSelectedListener mOnSelectedListener;
+        @Nullable
+        private IOnItemVisibilityChangedListener mOnItemVisibilityChangedListener;
+        @Nullable
+        private CarText mNoItemsMessage;
+
+        /**
+         * Sets the {@link OnItemVisibilityChangedListener} to call when the visible items in the
+         * list changes.
+         */
+        @NonNull
+        // TODO(shiufai): remove MissingGetterMatchingBuilder once listener is properly exposed.
+        @SuppressLint({"MissingGetterMatchingBuilder", "ExecutorRegistration"})
+        public Builder setOnItemsVisibilityChangeListener(
+                @Nullable OnItemVisibilityChangedListener itemVisibilityChangedListener) {
+            this.mOnItemVisibilityChangedListener =
+                    itemVisibilityChangedListener == null
+                            ? null
+                            : new OnItemVisibilityChangedListenerStub(
+                                    itemVisibilityChangedListener);
+            return this;
+        }
+
+        /**
+         * Marks the list as selectable and sets the {@link OnSelectedListener} to call when an
+         * item is selected by the user. Set to {@code null} to mark the list as non-selectable.
+         *
+         * <p>Selectable lists, where allowed by the template they are added to, automatically
+         * display
+         * an item in a selected state when selected by the user.
+         *
+         * <p>The items in the list define a mutually exclusive selection scope: only a single
+         * item will
+         * be selected at any given time.
+         *
+         * <p>The specific way in which the selection will be visualized depends on the template
+         * and the
+         * host implementation. For example, some templates may display the list as a radio button
+         * group, while others may highlight the selected item's background.
+         *
+         * @see #setSelectedIndex(int)
+         */
+        @NonNull
+        // TODO(shiufai): remove MissingGetterMatchingBuilder once listener is properly exposed.
+        @SuppressLint({"MissingGetterMatchingBuilder", "ExecutorRegistration"})
+        public Builder setSelectable(@Nullable OnSelectedListener onSelectedListener) {
+            this.mOnSelectedListener =
+                     null ? null : new OnSelectedListenerStub(
+                            onSelectedListener);
+            return this;
+        }
+
+        /**
+         * Sets the index of the item to show as selected.
+         *
+         * <p>By default and unless explicitly set with this method, the first item is selected.
+         *
+         * <p>If the list is not a selectable list set with {@link #setSelectable}, this value is
+         * ignored.
+         */
+        @NonNull
+        public Builder setSelectedIndex(int selectedIndex) {
+            if (selectedIndex < 0) {
+                throw new IllegalArgumentException(
+                        "The item index must be larger than or equal to 0.");
+            }
+            this.mSelectedIndex = selectedIndex;
+            return this;
+        }
+
+        /**
+         * Sets the text to display if the list is empty.
+         *
+         * <p>If the list is empty and the app does not explicitly set the message with this
+         * method, the
+         * host will show a default message.
+         */
+        @NonNull
+        public Builder setNoItemsMessage(@Nullable CharSequence noItemsMessage) {
+            this.mNoItemsMessage = noItemsMessage == null ? null : CarText.create(noItemsMessage);
+            return this;
+        }
+
+        /**
+         * Adds an item to the list.
+         *
+         * @throws NullPointerException if {@code item} is {@code null}.
+         */
+        @NonNull
+        public Builder addItem(@NonNull Item item) {
+            mItems.add(requireNonNull(item));
+            return this;
+        }
+
+        /** Clears any items that may have been added up to this point. */
+        @NonNull
+        public Builder clearItems() {
+            mItems.clear();
+            return this;
+        }
+
+        /**
+         * Constructs the item list defined by this builder.
+         *
+         * @throws IllegalStateException if the list is selectable but does not have any items.
+         * @throws IllegalStateException if the selected index is greater or equal to the size of
+         *                               the
+         *                               list.
+         * @throws IllegalStateException if the list is selectable and any items have either one of
+         *                               their {@link OnClickListener} or {@link Toggle} set.
+         */
+        @NonNull
+        public ItemList build() {
+            if (mOnSelectedListener != null) {
+                int listSize = mItems.size();
+                if (listSize == 0) {
+                    throw new IllegalStateException("A selectable list cannot be empty");
+                } else if (mSelectedIndex >= listSize) {
+                    throw new IllegalStateException(
+                            "The selected item index ("
+                                    + mSelectedIndex
+                                    + ") is larger than the size of the list ("
+                                    + listSize
+                                    + ")");
+                }
+
+                // Check that no items have disallowed elements if the list is selectable.
+                for (Object item : mItems) {
+                    if (getOnClickListener(item) != null) {
+                        throw new IllegalStateException(
+                                "Items that belong to selectable lists can't have an "
+                                        + "onClickListener. Use the"
+                                        + " OnSelectedListener of the list instead");
+                    }
+
+                    if (getToggle(item) != null) {
+                        throw new IllegalStateException(
+                                "Items that belong to selectable lists can't have a toggle");
+                    }
+                }
+            }
+
+            return new ItemList(this);
+        }
+    }
+
+    @Nullable
+    private static OnClickListenerWrapper getOnClickListener(Object item) {
+        if (item instanceof Row) {
+            return ((Row) item).getOnClickListener();
+        } else if (item instanceof GridItem) {
+            return ((GridItem) item).getOnClickListener();
+        }
+
+        return null;
+    }
+
+    @Nullable
+    private static Toggle getToggle(Object item) {
+        if (item instanceof Row) {
+            return ((Row) item).getToggle();
+        } else if (item instanceof GridItem) {
+            return ((GridItem) item).getToggle();
+        }
+
+        return null;
+    }
+
+    @Keep // We need to keep these stub for Bundler serialization logic.
+    private static class OnSelectedListenerStub extends IOnSelectedListener.Stub {
+        private final OnSelectedListener mOnSelectedListener;
+
+        private OnSelectedListenerStub(OnSelectedListener onSelectedListener) {
+            this.mOnSelectedListener = onSelectedListener;
+        }
+
+        @Override
+        public void onSelected(int index, IOnDoneCallback callback) {
+            RemoteUtils.dispatchHostCall(
+                    () -> mOnSelectedListener.onSelected(index), callback, "onSelectedListener");
+        }
+    }
+
+    /** Stub class for the {@link IOnItemVisibilityChangedListener} interface. */
+    @Keep // We need to keep these stub for Bundler serialization logic.
+    private static class OnItemVisibilityChangedListenerStub
+            extends IOnItemVisibilityChangedListener.Stub {
+        private final OnItemVisibilityChangedListener mOnItemVisibilityChangedListener;
+
+        private OnItemVisibilityChangedListenerStub(
+                OnItemVisibilityChangedListener onItemVisibilityChangedListener) {
+            this.mOnItemVisibilityChangedListener = onItemVisibilityChangedListener;
+        }
+
+        @Override
+        public void onItemVisibilityChanged(
+                int startIndexInclusive, int endIndexExclusive, IOnDoneCallback callback) {
+            RemoteUtils.dispatchHostCall(
+                    () ->
+                            mOnItemVisibilityChangedListener.onItemVisibilityChanged(
+                                    startIndexInclusive, endIndexExclusive),
+                    callback,
+                    "onItemVisibilityChanged");
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/LatLng.java b/car/app/app/src/main/java/androidx/car/app/model/LatLng.java
new file mode 100644
index 0000000..4164be5
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/LatLng.java
@@ -0,0 +1,96 @@
+/*
+ * 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.car.app.model;
+
+import static java.util.Objects.hash;
+import static java.util.Objects.requireNonNull;
+
+import android.location.Location;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Represents a geographical location with a latitude and a longitude. */
+public final class LatLng {
+    @Keep
+    private final double mLat;
+    @Keep
+    private final double mLng;
+
+    /** Returns a new instance of a {@link LatLng}. */
+    @NonNull
+    public static LatLng create(double latitude, double longitude) {
+        return new LatLng(latitude, longitude);
+    }
+
+    /**
+     * Returns a new instance of a {@link LatLng} with the same latitude and longitude contained in
+     * the given {@link Location}.
+     *
+     * @throws NullPointerException if {@code location} is {@code null}.
+     */
+    @NonNull
+    public static LatLng create(@NonNull Location location) {
+        requireNonNull(location);
+        return create(location.getLatitude(), location.getLongitude());
+    }
+
+    /** Returns the latitude of the location, in degrees. */
+    public double getLatitude() {
+        return mLat;
+    }
+
+    /** Returns the longitude of the location, in degrees. */
+    public double getLongitude() {
+        return mLng;
+    }
+
+    @Override
+    public String toString() {
+        return "[" + getLatitude() + ", " + getLongitude() + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return hash(mLat, mLng);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof LatLng)) {
+            return false;
+        }
+        LatLng otherLatLng = (LatLng) other;
+
+        return Double.doubleToLongBits(mLat) == Double.doubleToLongBits(otherLatLng.mLat)
+                && Double.doubleToLongBits(mLng) == Double.doubleToLongBits(otherLatLng.mLng);
+    }
+
+    private LatLng(double lat, double lng) {
+        this.mLat = lat;
+        this.mLng = lng;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private LatLng() {
+        this(0, 0);
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ListTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/ListTemplate.java
new file mode 100644
index 0000000..d3da538
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ListTemplate.java
@@ -0,0 +1,428 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_FULL_LIST;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.Screen;
+import androidx.car.app.utils.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A template representing a list of items.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in {@link Screen#getTemplate()}, this
+ * template is considered a refresh of a previous one if:
+ *
+ * <ul>
+ *   <li>The template title has not changed, and
+ *   <li>The previous template is in a loading state (see {@link Builder#setLoading}}, or the
+ *       {@link ItemList} structure between the templates have not changed. This means that if the
+ *       previous template has multiple {@link ItemList} sections, the new template must have the
+ *       same number of sections with the same headers. Further, the number of rows and the string
+ *       contents (title, texts, not counting spans) of each row must not have changed.
+ *   <li>For rows that contain a {@link Toggle}, updates to the title or texts are also allowed if
+ *       the toggle state has changed between the previous and new templates.
+ * </ul>
+ */
+public final class ListTemplate implements Template {
+    @Keep
+    private final boolean mIsLoading;
+    @Keep
+    @Nullable
+    private final CarText mTitle;
+    @Keep
+    @Nullable
+    private final Action mHeaderAction;
+    @Keep
+    @Nullable
+    private final ItemList mSingleList;
+    @Keep
+    private final List<SectionedItemList> mSectionLists;
+    @Keep
+    @Nullable
+    private final ActionStrip mActionStrip;
+
+    /** Constructs a new builder of {@link ListTemplate}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public boolean isLoading() {
+        return mIsLoading;
+    }
+
+    @Nullable
+    public CarText getTitle() {
+        return mTitle;
+    }
+
+    @Nullable
+    public Action getHeaderAction() {
+        return mHeaderAction;
+    }
+
+    @Nullable
+    public ItemList getSingleList() {
+        return mSingleList;
+    }
+
+    @NonNull
+    public List<SectionedItemList> getSectionLists() {
+        return mSectionLists;
+    }
+
+    @Nullable
+    public ActionStrip getActionStrip() {
+        return mActionStrip;
+    }
+
+    @Override
+    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+        requireNonNull(oldTemplate);
+
+        if (oldTemplate.getClass() != this.getClass()) {
+            return false;
+        }
+
+        ListTemplate old = (ListTemplate) oldTemplate;
+
+        if (!Objects.equals(old.getTitle(), getTitle())) {
+            return false;
+        }
+
+        if (old.mIsLoading) {
+            // Transition from a previous loading state is allowed.
+            return true;
+        } else if (mIsLoading) {
+            // Transition to a loading state is disallowed.
+            return false;
+        }
+
+        if (mSingleList != null && old.mSingleList != null) {
+            return mSingleList.isRefresh(old.mSingleList, logger);
+        } else {
+            if (mSectionLists.size() != old.mSectionLists.size()) {
+                return false;
+            }
+
+            for (int i = 0; i < mSectionLists.size(); i++) {
+                SectionedItemList section = mSectionLists.get(i);
+                SectionedItemList oldSection = old.mSectionLists.get(i);
+
+                if (!section.getHeader().equals(oldSection.getHeader())
+                        || !section.getItemList().isRefresh(oldSection.getItemList(), logger)) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "ListTemplate";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mIsLoading, mTitle, mHeaderAction, mSingleList, mSectionLists,
+                mActionStrip);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof ListTemplate)) {
+            return false;
+        }
+        ListTemplate otherTemplate = (ListTemplate) other;
+
+        return mIsLoading == otherTemplate.mIsLoading
+                && Objects.equals(mTitle, otherTemplate.mTitle)
+                && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+                && Objects.equals(mSingleList, otherTemplate.mSingleList)
+                && Objects.equals(mSectionLists, otherTemplate.mSectionLists)
+                && Objects.equals(mActionStrip, otherTemplate.mActionStrip);
+    }
+
+    private ListTemplate(Builder builder) {
+        mIsLoading = builder.mIsLoading;
+        mTitle = builder.mTitle;
+        mHeaderAction = builder.mHeaderAction;
+        mSingleList = builder.mSingleList;
+        mSectionLists = new ArrayList<>(builder.mSectionLists);
+        mActionStrip = builder.mActionStrip;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private ListTemplate() {
+        mIsLoading = false;
+        mTitle = null;
+        mHeaderAction = null;
+        mSingleList = null;
+        mSectionLists = Collections.emptyList();
+        mActionStrip = null;
+    }
+
+    /** A builder of {@link ListTemplate}. */
+    public static final class Builder {
+        private boolean mIsLoading;
+        @Nullable
+        private ItemList mSingleList;
+        private final List<SectionedItemList> mSectionLists = new ArrayList<>();
+        @Nullable
+        private CarText mTitle;
+        @Nullable
+        private Action mHeaderAction;
+        @Nullable
+        private ActionStrip mActionStrip;
+        private boolean mHasSelectableList;
+
+        /**
+         * Sets whether the template is in a loading state.
+         *
+         * <p>If set to {@code true}, the UI will display a loading indicator where the list content
+         * would be otherwise. The caller is expected to call {@link
+         * androidx.car.app.Screen#invalidate()} and send the new template content
+         * to the host once the data is ready.
+         *
+         * <p>If set to {@code false}, the UI will display the contents of the {@link ItemList}
+         * instance(s) added via {@link #setSingleList} or {@link #addList}.
+         */
+        @NonNull
+        public Builder setLoading(boolean isLoading) {
+            this.mIsLoading = isLoading;
+            return this;
+        }
+
+        /**
+         * Sets the {@link Action} that will be displayed in the header of the template, or
+         * {@code null}
+         * to not display an action.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template only supports either one of {@link Action#APP_ICON} and
+         * {@link Action#BACK} as
+         * a header {@link Action}.
+         *
+         * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setHeaderAction(@Nullable Action headerAction) {
+            ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+                    headerAction == null ? Collections.emptyList()
+                            : Collections.singletonList(headerAction));
+            this.mHeaderAction = headerAction;
+            return this;
+        }
+
+        /**
+         * Sets the {@link CharSequence} to show as the template's title, or {@code null} to not
+         * show a
+         * title.
+         */
+        @NonNull
+        public Builder setTitle(@Nullable CharSequence title) {
+            this.mTitle = title == null ? null : CarText.create(title);
+            return this;
+        }
+
+        /** Resets any list(s) that were added via {@link #setSingleList} or {@link #addList}. */
+        @NonNull
+        public Builder clearAllLists() {
+            mSingleList = null;
+            mSectionLists.clear();
+            mHasSelectableList = false;
+            return this;
+        }
+
+        /**
+         * Sets a single {@link ItemList} to show in the template.
+         *
+         * <p>Note that this list cannot be mixed with others added via {@link #addList}. If
+         * multiple
+         * lists were previously added, they will be cleared.
+         *
+         * @throws NullPointerException if {@code list} is null.
+         * @see #addList(ItemList, CharSequence)
+         */
+        @NonNull
+        public Builder setSingleList(@NonNull ItemList list) {
+            mSingleList = requireNonNull(list);
+            mSectionLists.clear();
+            mHasSelectableList = false;
+            return this;
+        }
+
+        /**
+         * Adds an {@link ItemList} to display in the template.
+         *
+         * <p>Use this method to add multiple {@link ItemList}s to the template. Each
+         * {@link ItemList}
+         * will be grouped under the given {@code header}. These lists cannot be mixed with an
+         * {@link
+         * ItemList} added via {@link #setSingleList}. If a single list was previously added, it
+         * will be
+         * cleared.
+         *
+         * <p>If the added {@link ItemList} contains a {@link ItemList.OnSelectedListener}, then it
+         * cannot be added alongside other {@link ItemList}(s).
+         *
+         * @throws NullPointerException     if {@code list} is null.
+         * @throws IllegalArgumentException if {@code list} is empty.
+         * @throws IllegalArgumentException if {@code list}'s {@link
+         *                                  ItemList.OnItemVisibilityChangedListener} is set.
+         * @throws NullPointerException     if {@code header} is null.
+         * @throws IllegalArgumentException if {@code header} is empty.
+         * @throws IllegalArgumentException if a selectable list is added alongside other lists.
+         */
+        @NonNull
+        // TODO(shiufai): consider rename to match getter's name.
+        @SuppressLint("MissingGetterMatchingBuilder")
+        public Builder addList(@NonNull ItemList list, @NonNull CharSequence header) {
+            if (requireNonNull(header).length() == 0) {
+                throw new IllegalArgumentException("Header cannot be empty");
+            }
+            CarText headerText = CarText.create(header);
+
+            boolean isSelectableList = list.getOnSelectedListener() != null;
+            if (mHasSelectableList || (isSelectableList && !mSectionLists.isEmpty())) {
+                throw new IllegalArgumentException(
+                        "A selectable list cannot be added alongside any other lists");
+            }
+            mHasSelectableList = isSelectableList;
+
+            if (list.getItems().isEmpty()) {
+                throw new IllegalArgumentException("List cannot be empty");
+            }
+
+            if (list.getOnItemsVisibilityChangeListener() != null) {
+                throw new IllegalArgumentException(
+                        "OnItemVisibilityChangedListener in the list is disallowed");
+            }
+
+            mSingleList = null;
+            mSectionLists.add(SectionedItemList.create(list, headerText));
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(LIBRARY)
+        @NonNull
+        public Builder addListForTesting(@NonNull ItemList list, @NonNull CharSequence header) {
+            mSingleList = null;
+            mSectionLists.add(SectionedItemList.create(list, CarText.create(header)));
+            return this;
+        }
+
+        /**
+         * Sets the {@link ActionStrip} for this template, or {@code null} to not display an {@link
+         * ActionStrip}.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
+         * {@link Action}s, one of them can contain a title as set via
+         * {@link Action.Builder#setTitle}. Otherwise, only {@link Action}s with icons are allowed.
+         *
+         * @throws IllegalArgumentException if {@code actionStrip} does not meet the requirements.
+         */
+        @NonNull
+        public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+            ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+                    actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+            this.mActionStrip = actionStrip;
+            return this;
+        }
+
+        /**
+         * Constructs the template defined by this builder.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 6 {@link Row}s total in the {@link ItemList}(s). The host will
+         * ignore any items over that limit. Each {@link Row}s can add up to 2 lines of texts via
+         * {@link Row.Builder#addText}.
+         *
+         * <p>Either a header {@link Action} or the title must be set on the template.
+         *
+         * @throws IllegalStateException    if the template is in a loading state but there are
+         *                                  lists added, or vice versa.
+         * @throws IllegalArgumentException if the added {@link ItemList}(s) do not meet the
+         *                                  template's requirements.
+         * @throws IllegalStateException    if the template does not have either a title or header
+         *                                  {@link Action} set.
+         */
+        @NonNull
+        public ListTemplate build() {
+            boolean hasList = mSingleList != null || !mSectionLists.isEmpty();
+            if (mIsLoading == hasList) {
+                throw new IllegalStateException(
+                        "Template is in a loading state but lists are added, or vice versa");
+            }
+
+            if (hasList) {
+                if (!mSectionLists.isEmpty()) {
+                    ROW_LIST_CONSTRAINTS_FULL_LIST.validateOrThrow(mSectionLists);
+                } else if (mSingleList != null) {
+                    ROW_LIST_CONSTRAINTS_FULL_LIST.validateOrThrow(mSingleList);
+                }
+            }
+
+            if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+                throw new IllegalStateException("Either the title or header action must be set");
+            }
+
+            return new ListTemplate(this);
+        }
+
+        /** @hide */
+        @RestrictTo(LIBRARY)
+        @NonNull
+        public ListTemplate buildForTesting() {
+            return new ListTemplate(this);
+        }
+
+        private Builder() {
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
new file mode 100644
index 0000000..90beda5
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
@@ -0,0 +1,339 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+import android.util.Log;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.constraints.CarIconConstraints;
+import androidx.car.app.utils.Logger;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A template for displaying a message and associated actions.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in
+ * {@link androidx.car.app.Screen#getTemplate()}, this template is
+ * considered a refresh of a previous one if the title and messages have not changed.
+ */
+public final class MessageTemplate implements Template {
+    @Keep
+    @Nullable
+    private final CarText mTitle;
+    @Keep
+    @Nullable
+    private final CarText mMessage;
+    @Keep
+    @Nullable
+    private final CarText mDebugMessage;
+    @Keep
+    @Nullable
+    private final CarIcon mIcon;
+    @Keep
+    @Nullable
+    private final Action mHeaderAction;
+    @Keep
+    @Nullable
+    private final ActionList mActionList;
+
+    /** Constructs a new builder of {@link MessageTemplate}. */
+    @NonNull
+    public static Builder builder(@NonNull CharSequence message) {
+        return new Builder(requireNonNull(message));
+    }
+
+    @Nullable
+    public CarText getTitle() {
+        return mTitle;
+    }
+
+    @Nullable
+    public Action getHeaderAction() {
+        return mHeaderAction;
+    }
+
+    @NonNull
+    public CarText getMessage() {
+        return Objects.requireNonNull(mMessage);
+    }
+
+    @Nullable
+    public CarText getDebugMessage() {
+        return mDebugMessage;
+    }
+
+    @Nullable
+    public CarIcon getIcon() {
+        return mIcon;
+    }
+
+    @Nullable
+    public ActionList getActionList() {
+        return mActionList;
+    }
+
+    @Override
+    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+        requireNonNull(oldTemplate);
+
+        if (oldTemplate.getClass() != this.getClass()) {
+            return false;
+        }
+
+        MessageTemplate old = (MessageTemplate) oldTemplate;
+        return Objects.equals(old.getTitle(), getTitle())
+                && Objects.equals(old.getDebugMessage(), getDebugMessage())
+                && Objects.equals(old.getMessage(), getMessage());
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "MessageTemplate";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mTitle, mMessage, mDebugMessage, mHeaderAction, mActionList, mIcon);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof MessageTemplate)) {
+            return false;
+        }
+        MessageTemplate otherTemplate = (MessageTemplate) other;
+
+        return Objects.equals(mTitle, otherTemplate.mTitle)
+                && Objects.equals(mMessage, otherTemplate.mMessage)
+                && Objects.equals(mDebugMessage, otherTemplate.mDebugMessage)
+                && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+                && Objects.equals(mActionList, otherTemplate.mActionList)
+                && Objects.equals(mIcon, otherTemplate.mIcon);
+    }
+
+    private MessageTemplate(Builder builder) {
+        mTitle = builder.mTitle;
+        mMessage = builder.mMessage;
+        mDebugMessage = builder.mDebugMessage;
+        mIcon = builder.mIcon;
+        mHeaderAction = builder.mHeaderAction;
+        mActionList = builder.mActionList;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private MessageTemplate() {
+        mTitle = null;
+        mMessage = null;
+        mDebugMessage = null;
+        mIcon = null;
+        mHeaderAction = null;
+        mActionList = null;
+    }
+
+    /** A builder of {@link MessageTemplate}. */
+    public static final class Builder {
+        @Nullable
+        private CarText mTitle;
+        private CarText mMessage;
+        @Nullable
+        private CarText mDebugMessage;
+        @Nullable
+        private CarIcon mIcon;
+        @Nullable
+        private Action mHeaderAction;
+        @Nullable
+        private ActionList mActionList;
+        @Nullable
+        private Throwable mDebugCause;
+        @Nullable
+        private String mDebugString;
+
+        /**
+         * Sets the {@link CharSequence} to show as the template's title, or {@code null} to not
+         * show a
+         * title.
+         */
+        @NonNull
+        public Builder setTitle(@Nullable CharSequence title) {
+            this.mTitle = title == null ? null : CarText.create(title);
+            return this;
+        }
+
+        /**
+         * Sets the {@link CharSequence} to display as the message in the template.
+         *
+         * @throws NullPointerException if {@code message} is null.
+         */
+        @NonNull
+        public Builder setMessage(@NonNull CharSequence message) {
+            this.mMessage = CarText.create(requireNonNull(message));
+            return this;
+        }
+
+        /**
+         * Sets a {@link Throwable} for debugging purposes, or {@code null} to not show it.
+         *
+         * <p>The cause will be displayed along with the message set in {@link #setDebugMessage}.
+         *
+         * <p>The host may choose to not display this debugging information if it doesn't deem it
+         * appropriate, for example, when running on a production environment rather than in a
+         * simulator
+         * such as the Desktop Head Unit.
+         */
+        @NonNull
+        // Suppress as the cause is transformed into a message before transport.
+        @SuppressLint("MissingGetterMatchingBuilder")
+        public Builder setDebugCause(@Nullable Throwable cause) {
+            this.mDebugCause = cause;
+            return this;
+        }
+
+        /**
+         * Sets a debug message for debugging purposes, or {@code null} to not show a debug message.
+         *
+         * <p>The debug message will be displayed along with the cause set in
+         * {@link #setDebugCause}.
+         *
+         * <p>The host may choose to not display this debugging information if it doesn't deem it
+         * appropriate, for example, when running on a production environment rather than in a
+         * simulator
+         * such as the Desktop Head Unit.
+         */
+        @NonNull
+        public Builder setDebugMessage(@Nullable String debugMessage) {
+            this.mDebugString = debugMessage;
+            return this;
+        }
+
+        /**
+         * Sets the icon to be displayed along with the message, or {@code null} to not display any
+         * icons.
+         *
+         * <h4>Icon Sizing Guidance</h4>
+         *
+         * The provided icon should have a maximum size of 64 x 64 dp. If the icon exceeds this
+         * maximum
+         * size in either one of the dimensions, it will be scaled down and centered inside the
+         * bounding
+         * box while preserving the aspect ratio.
+         *
+         * <p>See {@link CarIcon} for more details related to providing icon and image resources
+         * that
+         * work with different car screen pixel densities.
+         */
+        @NonNull
+        public Builder setIcon(@Nullable CarIcon icon) {
+            CarIconConstraints.DEFAULT.validateOrThrow(icon);
+            this.mIcon = icon;
+            return this;
+        }
+
+        /**
+         * Sets the {@link Action} that will be displayed in the header of the template, or
+         * {@code null}
+         * to not display an action.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template only supports either either one of {@link Action#APP_ICON} and {@link
+         * Action#BACK} as a header {@link Action}.
+         *
+         * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setHeaderAction(@Nullable Action headerAction) {
+            ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+                    headerAction == null ? Collections.emptyList()
+                            : Collections.singletonList(headerAction));
+            this.mHeaderAction = headerAction;
+            return this;
+        }
+
+        /**
+         * Sets a list of {@link Action}s to display along with the message.
+         *
+         * <p>Any actions above the maximum limit of 2 will be ignored.
+         *
+         * @throws NullPointerException if {@code actions} is {@code null}.
+         */
+        @NonNull
+        // TODO(shiufai): consider rename to match getter's name (e.g. setActionList or getActions).
+        @SuppressLint("MissingGetterMatchingBuilder")
+        public Builder setActions(@NonNull List<Action> actions) {
+            mActionList = ActionList.create(requireNonNull(actions));
+            return this;
+        }
+
+        /**
+         * Constructs the {@link MessageTemplate} defined by this builder.
+         *
+         * <h4>Requirements</h4>
+         *
+         * A non-empty message must be set on the template with {@link
+         * Builder#setMessage(CharSequence)}.
+         *
+         * <p>Either a header {@link Action} or title must be set on the template.
+         *
+         * @throws IllegalStateException if the message is empty.
+         * @throws IllegalStateException if the template does not have either a title or header
+         *                               {@link
+         *                               Action} set.
+         */
+        @NonNull
+        public MessageTemplate build() {
+            if (mMessage.isEmpty()) {
+                throw new IllegalStateException("Message cannot be empty");
+            }
+
+            String debugString = this.mDebugString == null ? "" : this.mDebugString;
+            if (!debugString.isEmpty() && mDebugCause != null) {
+                debugString += "\n";
+            }
+            debugString += Log.getStackTraceString(mDebugCause);
+            if (!debugString.isEmpty()) {
+                mDebugMessage = CarText.create(debugString);
+            }
+
+            if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+                throw new IllegalStateException("Either the title or header action must be set");
+            }
+
+            return new MessageTemplate(this);
+        }
+
+        private Builder(CharSequence message) {
+            this.mMessage = CarText.create(message);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Metadata.java b/car/app/app/src/main/java/androidx/car/app/model/Metadata.java
new file mode 100644
index 0000000..429aab4
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Metadata.java
@@ -0,0 +1,122 @@
+/*
+ * 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.car.app.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/** A metadata class used for attaching additional properties to models. */
+public class Metadata {
+    /** An empty {@link Metadata} instance. */
+    public static final Metadata EMPTY_METADATA = new Builder().build();
+
+    @Keep
+    @Nullable
+    private final Place mPlace;
+
+    /** Constructs a new builder of a {@link Metadata} instance. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Constructs a new instance of {@link Metadata} containing a {@link Place}.
+     *
+     * @throws NullPointerException if {@code place} is {@code null}.
+     * @see Builder#setPlace(Place)
+     */
+    @NonNull
+    public static Metadata ofPlace(@NonNull Place place) {
+        return new Builder().setPlace(requireNonNull(place)).build();
+    }
+
+    /** Returns a new {@link Builder} with the data from this {@link Metadata} instance. */
+    @NonNull
+    public Builder newBuilder() {
+        return new Builder(this);
+    }
+
+    @Nullable
+    public Place getPlace() {
+        return mPlace;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mPlace);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof Metadata)) {
+            return false;
+        }
+        Metadata otherMetadata = (Metadata) other;
+
+        return Objects.equals(mPlace, otherMetadata.mPlace);
+    }
+
+    private Metadata(Builder builder) {
+        mPlace = builder.mPlace;
+    }
+
+    /** Default constructor for serialization. */
+    private Metadata() {
+        mPlace = null;
+    }
+
+    /** A builder for {@link Metadata}. */
+    public static final class Builder {
+        @Nullable
+        private Place mPlace;
+
+        /**
+         * Sets a {@link Place} used for showing {@link Distance} and {@link PlaceMarker}
+         * information,
+         * or {@code null} if no {@link Place} information is available.
+         */
+        @NonNull
+        public Builder setPlace(@Nullable Place place) {
+            this.mPlace = place;
+            return this;
+        }
+
+        /**
+         * Returns a {@link Metadata} instance defined by this builder.
+         */
+        @NonNull
+        public Metadata build() {
+            return new Metadata(this);
+        }
+
+        private Builder() {
+        }
+
+        private Builder(Metadata metadata) {
+            this.mPlace = metadata.mPlace;
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ModelUtils.java b/car/app/app/src/main/java/androidx/car/app/model/ModelUtils.java
new file mode 100644
index 0000000..240e097
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ModelUtils.java
@@ -0,0 +1,168 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.text.style.CharacterStyle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.CarText.SpanWrapper;
+
+import java.util.List;
+
+/**
+ * Utility class for common operations on the car app models
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public final class ModelUtils {
+    /**
+     * Checks whether all non-browsable rows have attached at least one {@link DistanceSpan} in
+     * either the title or secondary text.
+     *
+     * @throws IllegalArgumentException if any non-browsable row does not have a
+     *                                  {@link DistanceSpan} instance.
+     */
+    public static void validateAllNonBrowsableRowsHaveDistance(@NonNull List<Object> rows) {
+        int spanSetCount = 0;
+        int nonBrowsableRowCount = 0;
+        for (Object rowObj : rows) {
+            Row row = (Row) rowObj;
+
+            if (!row.isBrowsable()) {
+                nonBrowsableRowCount++;
+            }
+
+            if (checkRowHasSpanType(row, DistanceSpan.class)) {
+                spanSetCount++;
+            }
+        }
+
+        if (nonBrowsableRowCount > spanSetCount) {
+            throw new IllegalArgumentException(
+                    "All non-browsable rows must have a distance span attached to either its "
+                            + "title or texts");
+        }
+    }
+
+    /**
+     * Checks whether all rows have attached at least one {@link DurationSpan} or
+     * {@link DistanceSpan }in either the title or secondary text.
+     *
+     * @throws IllegalArgumentException if any non-browsable row does not have either a {@link
+     *                                  DurationSpan} or {@link DistanceSpan} instance.
+     */
+    public static void validateAllRowsHaveDistanceOrDuration(@NonNull List<Object> rows) {
+        for (Object rowObj : rows) {
+            Row row = (Row) rowObj;
+            if (!(checkRowHasSpanType(row, DistanceSpan.class)
+                    || checkRowHasSpanType(row, DurationSpan.class))) {
+                throw new IllegalArgumentException(
+                        "All rows must have either a distance or duration span attached to either"
+                                + " its title or"
+                                + " texts");
+            }
+        }
+    }
+
+    /**
+     * Checks whether all rows have only small-sized images if they are set.
+     *
+     * @throws IllegalArgumentException if an image set in any rows is using {@link
+     *                                  Row#IMAGE_TYPE_LARGE}.
+     */
+    public static void validateAllRowsHaveOnlySmallImages(@NonNull List<Object> rows) {
+        for (Object rowObj : rows) {
+            Row row = (Row) rowObj;
+            if (row.getImage() != null && row.getRowImageType() == Row.IMAGE_TYPE_LARGE) {
+                throw new IllegalArgumentException("Rows must only use small-sized images");
+            }
+        }
+    }
+
+    /**
+     * Checks whether any rows have both a marker and an image.
+     *
+     * @throws IllegalArgumentException if both a marker and an image are set in a row.
+     */
+    public static void validateNoRowsHaveBothMarkersAndImages(@NonNull List<Object> rows) {
+        for (Object rowObj : rows) {
+            Row row = (Row) rowObj;
+
+            boolean hasImage = row.getImage() != null;
+            Place place = row.getMetadata().getPlace();
+            boolean hasMarker = place != null && place.getMarker() != null;
+
+            if (hasImage && hasMarker) {
+                throw new IllegalArgumentException("Rows can't have both a marker and an image");
+            }
+        }
+    }
+
+    /**
+     * Returns {@code true} if the given row has a span of the given type, {@code false} otherwise.
+     */
+    private static boolean checkRowHasSpanType(Row row, Class<? extends CharacterStyle> spanType) {
+        CarText title = row.getTitle();
+        if (checkCarTextHasSpanType(title, spanType)) {
+            return true;
+        }
+
+        List<CarText> texts = row.getTexts();
+        for (int i = 0; i < texts.size(); i++) {
+            CarText text = texts.get(i);
+            if (checkCarTextHasSpanType(text, spanType)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns {@code true} if the given {@link CarText} has a span of the given type, {@code false}
+     * otherwise.
+     */
+    private static boolean checkCarTextHasSpanType(
+            CarText carText, Class<? extends CharacterStyle> spanType) {
+        if (carText.isEmpty()) {
+            return false;
+        }
+        String text = requireNonNull(carText.getText());
+
+        List<SpanWrapper> spans = carText.getSpans();
+        for (int i = 0; i < spans.size(); i++) {
+            SpanWrapper wrapper = spans.get(i);
+            Object spanObj = wrapper.span;
+            if (spanType.isInstance(spanObj)
+                    && wrapper.start >= 0
+                    && wrapper.start != wrapper.end
+                    && wrapper.start < text.length()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private ModelUtils() {
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java b/car/app/app/src/main/java/androidx/car/app/model/OnClickListener.java
similarity index 71%
copy from camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java
copy to car/app/app/src/main/java/androidx/car/app/model/OnClickListener.java
index d6b6436..f36ffca 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/package-info.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnClickListener.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-/**
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-package androidx.camera.core.impl.quirk;
+package androidx.car.app.model;
 
-import androidx.annotation.RestrictTo;
+/** A listener of click events. */
+public interface OnClickListener {
+    /** Notifies that a click happened. */
+    void onClick();
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/OnClickListenerWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/OnClickListenerWrapper.java
new file mode 100644
index 0000000..789dd53
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnClickListenerWrapper.java
@@ -0,0 +1,96 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.utils.RemoteUtils;
+
+/**
+ * Internal state object to pass additional state along with the wrapped {@code IOnClickListener}.
+ */
+// TODO(shiufai): Replace code tag with the correct AIDL wrapper.
+public class OnClickListenerWrapper {
+
+    @Keep
+    @Nullable
+    private final IOnClickListener mListener;
+    @Keep
+    private final boolean mIsParkedOnly;
+
+    /**
+     * @hide
+     */
+    // TODO(shiufai): re-surface this API with a wrapper around the AIDL class.
+    @RestrictTo(LIBRARY)
+    @NonNull
+    public IOnClickListener getListener() {
+        return requireNonNull(mListener);
+    }
+
+    /**
+     * Whether the click listener is for parked-only scenarios.
+     */
+    public boolean isParkedOnly() {
+        return mIsParkedOnly;
+    }
+
+    /**
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(LIBRARY)
+    @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+    public static OnClickListenerWrapper create(@NonNull OnClickListener listener) {
+        return new OnClickListenerWrapper(
+                new OnClickListenerStub(listener), listener instanceof ParkedOnlyOnClickListener);
+    }
+
+    private OnClickListenerWrapper(IOnClickListener listener, boolean isParkedOnly) {
+        this.mListener = listener;
+        this.mIsParkedOnly = isParkedOnly;
+    }
+
+    /** For serialization. */
+    private OnClickListenerWrapper() {
+        mListener = null;
+        mIsParkedOnly = false;
+    }
+
+    @Keep // We need to keep these stub for Bundler serialization logic.
+    private static class OnClickListenerStub extends IOnClickListener.Stub {
+        private final OnClickListener mOnClickListener;
+
+        private OnClickListenerStub(OnClickListener onClickListener) {
+            this.mOnClickListener = onClickListener;
+        }
+
+        @Override
+        public void onClick(IOnDoneCallback callback) {
+            RemoteUtils.dispatchHostCall(mOnClickListener::onClick, callback, "onClick");
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Pane.java b/car/app/app/src/main/java/androidx/car/app/model/Pane.java
new file mode 100644
index 0000000..0670466
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Pane.java
@@ -0,0 +1,220 @@
+/*
+ * 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.car.app.model;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.utils.Logger;
+import androidx.car.app.utils.ValidationUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a list of rows used for displaying informational content and a set of {@link Action}s
+ * that users can perform based on such content.
+ */
+public final class Pane {
+    @Keep
+    @Nullable
+    private final ActionList mActionList;
+    @Keep
+    private final List<Object> mRows;
+    @Keep
+    private final boolean mIsLoading;
+
+    /** Constructs a new builder of {@link Pane}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns the list of {@link Action}s displayed alongside the {@link Row}s in this pane.
+     */
+    @Nullable
+    public ActionList getActionList() {
+        return mActionList;
+    }
+
+    /**
+     * Returns the list of {@link Row} objects that make up the {@link Pane}.
+     */
+    @NonNull
+    public List<Object> getRows() {
+        return mRows;
+    }
+
+    /**
+     * Returns the {@code true} if the {@link Pane} is loading.*
+     */
+    public boolean isLoading() {
+        return mIsLoading;
+    }
+
+    /**
+     * Returns {@code true} if this {@link Pane} instance is determined to be a refresh of the given
+     * pane, or {@code false} otherwise.
+     *
+     * <p>A pane is considered a refresh if:
+     *
+     * <ul>
+     *   <li>The other pane is in a loading state, or
+     *   <li>The row size and string contents of the two panes are the same.
+     * </ul>
+     */
+    public boolean isRefresh(@Nullable Pane other, @NonNull Logger logger) {
+        if (other == null) {
+            return false;
+        } else if (other.isLoading()) {
+            return true;
+        } else if (isLoading()) {
+            return false;
+        }
+
+        return ValidationUtils.itemsHaveSameContent(other.getRows(), getRows(), logger);
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[ rows: "
+                + (mRows != null ? mRows.toString() : null)
+                + ", action list: "
+                + mActionList
+                + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mRows, mActionList, mIsLoading);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof Pane)) {
+            return false;
+        }
+        Pane otherPane = (Pane) other;
+
+        return mIsLoading == otherPane.mIsLoading
+                && Objects.equals(mActionList, otherPane.mActionList)
+                && Objects.equals(mRows, otherPane.mRows);
+    }
+
+    private Pane(Builder builder) {
+        mRows = new ArrayList<>(builder.mRows);
+        mActionList = builder.mActionList;
+        mIsLoading = builder.mIsLoading;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private Pane() {
+        mRows = Collections.emptyList();
+        mActionList = null;
+        mIsLoading = false;
+    }
+
+    /** A builder of {@link Pane}. */
+    public static final class Builder {
+        private final List<Object> mRows = new ArrayList<>();
+        @Nullable
+        private ActionList mActionList;
+        private boolean mIsLoading;
+
+        /**
+         * Sets whether the {@link Pane} is in a loading state.
+         *
+         * <p>If set to {@code true}, the UI will display a loading indicator where the list content
+         * would be otherwise. The caller is expected to call {@link
+         * androidx.car.app.Screen#invalidate()} and send the new template content
+         * to the host once the data is ready. If set to {@code false}, the UI shows the actual row
+         * contents.
+         *
+         * @see #build
+         */
+        @NonNull
+        public Builder setLoading(boolean isLoading) {
+            this.mIsLoading = isLoading;
+            return this;
+        }
+
+        /**
+         * Adds a row to display in the list.
+         *
+         * @throws NullPointerException if {@code row} is {@code null}.
+         */
+        @NonNull
+        public Builder addRow(@NonNull Row row) {
+            mRows.add(requireNonNull(row));
+            return this;
+        }
+
+        /** Clears any rows that may have been added with {@link #addRow(Row)} up to this point. */
+        @NonNull
+        public Builder clearRows() {
+            mRows.clear();
+            return this;
+        }
+
+        /**
+         * Sets multiple {@link Action}s to display alongside the rows in the pane.
+         *
+         * <p>By default, no actions are displayed.
+         *
+         * @throws NullPointerException if {@code actions} is {@code null}.
+         */
+        @NonNull
+        // TODO(shiufai): consider rename to match getter's name (e.g. setActionList or getActions).
+        @SuppressLint("MissingGetterMatchingBuilder")
+        public Builder setActions(@NonNull List<Action> actions) {
+            mActionList = ActionList.create(requireNonNull(actions));
+            return this;
+        }
+
+        /**
+         * Constructs the row list defined by this builder.
+         *
+         * @throws IllegalStateException if the pane is in loading state and also contains rows, or
+         *                               vice-versa.
+         */
+        @NonNull
+        public Pane build() {
+            int size = size();
+            if (size > 0 == mIsLoading) {
+                throw new IllegalStateException(
+                        "The pane is set to loading but is not empty, or vice versa");
+            }
+
+            return new Pane(this);
+        }
+
+        private int size() {
+            return mRows.size();
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java
new file mode 100644
index 0000000..37597b6
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java
@@ -0,0 +1,263 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_PANE;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.utils.Logger;
+
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * A template that displays a {@link Pane}.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in
+ * {@link androidx.car.app.Screen#getTemplate()}, this template is considered a refresh of a
+ * previous one if:
+ *
+ * <ul>
+ *   <li>The template title has not changed, and
+ *   <li>The previous template is in a loading state (see {@link Pane.Builder#setLoading}, or the
+ *       number of rows and the string contents (title, texts, not counting spans) of each row
+ *       between the previous and new {@link Pane}s have not changed.
+ * </ul>
+ */
+public final class PaneTemplate implements Template {
+    @Keep
+    @Nullable
+    private final CarText mTitle;
+    @Keep
+    @Nullable
+    private final Pane mPane;
+    @Keep
+    @Nullable
+    private final Action mHeaderAction;
+    @Keep
+    @Nullable
+    private final ActionStrip mActionStrip;
+
+    /**
+     * Constructs a new builder of {@link PaneTemplate}.
+     *
+     * @throws NullPointerException if {@code pane} is {@code null}
+     */
+    @NonNull
+    public static Builder builder(@NonNull Pane pane) {
+        return new Builder(requireNonNull(pane));
+    }
+
+    @Nullable
+    public CarText getTitle() {
+        return mTitle;
+    }
+
+    @Nullable
+    public Action getHeaderAction() {
+        return mHeaderAction;
+    }
+
+    @NonNull
+    public Pane getPane() {
+        return requireNonNull(mPane);
+    }
+
+    @Nullable
+    public ActionStrip getActionStrip() {
+        return mActionStrip;
+    }
+
+    @Override
+    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+        if (oldTemplate.getClass() != this.getClass()) {
+            return false;
+        }
+
+        PaneTemplate old = (PaneTemplate) oldTemplate;
+        return Objects.equals(old.getTitle(), getTitle()) && getPane().isRefresh(old.getPane(),
+                logger);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "PaneTemplate";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mTitle, mPane, mHeaderAction, mActionStrip);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof PaneTemplate)) {
+            return false;
+        }
+        PaneTemplate otherTemplate = (PaneTemplate) other;
+
+        return Objects.equals(mTitle, otherTemplate.mTitle)
+                && Objects.equals(mPane, otherTemplate.mPane)
+                && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+                && Objects.equals(mActionStrip, otherTemplate.mActionStrip);
+    }
+
+    private PaneTemplate(Builder builder) {
+        mTitle = builder.mTitle;
+        mPane = builder.mPane;
+        mHeaderAction = builder.mHeaderAction;
+        mActionStrip = builder.mActionStrip;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private PaneTemplate() {
+        mTitle = null;
+        mPane = null;
+        mHeaderAction = null;
+        mActionStrip = null;
+    }
+
+    /** A builder of {@link PaneTemplate}. */
+    public static final class Builder {
+        @Nullable
+        private CarText mTitle;
+        private Pane mPane;
+        @Nullable
+        private Action mHeaderAction;
+        @Nullable
+        private ActionStrip mActionStrip;
+
+        private Builder(Pane pane) {
+            this.mPane = pane;
+        }
+
+        /**
+         * Sets the {@link CharSequence} to show as the template's title, or {@code null} to not
+         * show a
+         * title.
+         */
+        @NonNull
+        public Builder setTitle(@Nullable CharSequence title) {
+            this.mTitle = title == null ? null : CarText.create(title);
+            return this;
+        }
+
+        /**
+         * Sets the {@link Action} that will be displayed in the header of the template, or
+         * {@code null}
+         * to not display an action.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template only supports either either one of {@link Action#APP_ICON} and {@link
+         * Action#BACK} as a header {@link Action}.
+         *
+         * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setHeaderAction(@Nullable Action headerAction) {
+            ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+                    headerAction == null ? Collections.emptyList()
+                            : Collections.singletonList(headerAction));
+            this.mHeaderAction = headerAction;
+            return this;
+        }
+
+        /**
+         * Sets the {@link Pane} to display in the template.
+         *
+         * @throws NullPointerException if {@code pane} is {@code null}.
+         */
+        @NonNull
+        public Builder setPane(@NonNull Pane pane) {
+            this.mPane = requireNonNull(pane);
+            return this;
+        }
+
+        /**
+         * Sets the {@link ActionStrip} for this template.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
+         * {@link Action}s, one of them can contain a title as set via
+         * {@link Action.Builder#setTitle}.
+         * Otherwise, only {@link Action}s with icons are allowed.
+         *
+         * @throws IllegalArgumentException if {@code actionStrip} does not meet the requirements.
+         */
+        @NonNull
+        public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+            ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+                    actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+            this.mActionStrip = actionStrip;
+            return this;
+        }
+
+        /**
+         * Constructs the template defined by this builder.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 2 {@link Row}s and 2 {@link Action}s in the {@link Pane}.
+         * The host
+         * will ignore any rows over that limit. Each {@link Row}s can add up to 2 lines of texts
+         * via
+         * {@link Row.Builder#addText} and cannot contain either a {@link Toggle} or a {@link
+         * OnClickListener}.
+         *
+         * <p>Either a header {@link Action} or title must be set on the template.
+         *
+         * @throws IllegalArgumentException if the {@link Pane} does not meet the requirements.
+         * @throws IllegalStateException    if the template does not have either a title or header
+         *                                  {@link
+         *                                  Action} set.
+         */
+        @NonNull
+        public PaneTemplate build() {
+            ROW_LIST_CONSTRAINTS_PANE.validateOrThrow(mPane);
+
+            if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+                throw new IllegalStateException("Either the title or header action must be set");
+            }
+
+            return new PaneTemplate(this);
+        }
+
+        /** @hide */
+        @RestrictTo(LIBRARY)
+        @NonNull
+        public PaneTemplate buildForTesting() {
+            return new PaneTemplate(this);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ParkedOnlyOnClickListener.java b/car/app/app/src/main/java/androidx/car/app/model/ParkedOnlyOnClickListener.java
new file mode 100644
index 0000000..951e525
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/ParkedOnlyOnClickListener.java
@@ -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.
+ */
+
+package androidx.car.app.model;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+
+/**
+ * An {@link OnClickListener} that wraps another one and executes its {@link #onClick} method only
+ * when the car is parked.
+ *
+ * <p>When the car is not parked, the handler won't be executed and the host will display a message
+ * to the user indicating that the action can only be used while parked.
+ *
+ * <p>Actions that direct the users to their phones must only execute while parked. This class
+ * should be used for wrapping any click listeners that invoke such actions.
+ *
+ * <p>Example:
+ *
+ * <pre>{@code
+ * builder.setOnClickListener(ParkedOnlyOnClickListener.create(
+ *     () -> myClickAction()));
+ * }</pre>
+ */
+// Lint check wants this to be renamed *Callback.
+@SuppressLint("ListenerInterface")
+public final class ParkedOnlyOnClickListener implements OnClickListener {
+    @Keep
+    private final OnClickListener mListener;
+
+    @Override
+    public void onClick() {
+        mListener.onClick();
+    }
+
+    /**
+     * Constructs a new instance of a {@link ParkedOnlyOnClickListener}.
+     *
+     * @throws NullPointerException if {@code listener} is {@code null}.
+     */
+    @NonNull
+    @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+    public static ParkedOnlyOnClickListener create(@NonNull OnClickListener listener) {
+        return new ParkedOnlyOnClickListener(requireNonNull(listener));
+    }
+
+    private ParkedOnlyOnClickListener(OnClickListener listener) {
+        this.mListener = listener;
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Place.java b/car/app/app/src/main/java/androidx/car/app/model/Place.java
new file mode 100644
index 0000000..cdef8bd
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Place.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.car.app.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/** Represents a geographical location and additional information on how to display it. */
+public class Place {
+    @Keep
+    @Nullable
+    private final LatLng mLatLng;
+    @Keep
+    @Nullable
+    private final PlaceMarker mMarker;
+
+    /**
+     * Create a builder for a {@link Place} instance.
+     *
+     * @param latLng the geographical location associated with the place.
+     * @throws NullPointerException if {@code latLng} is {@code null}.
+     */
+    @NonNull
+    public static Builder builder(@NonNull LatLng latLng) {
+        return new Builder(requireNonNull(latLng));
+    }
+
+    /** Returns a {@link Builder} instance with the same data as this {@link Place} instance. */
+    @NonNull
+    public Builder newBuilder() {
+        return new Builder(this);
+    }
+
+    @Nullable
+    public PlaceMarker getMarker() {
+        return mMarker;
+    }
+
+    @NonNull
+    public LatLng getLatLng() {
+        return requireNonNull(mLatLng);
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[ latlng: " + mLatLng + ", marker: " + mMarker + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mLatLng, mMarker);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof Place)) {
+            return false;
+        }
+        Place otherPlace = (Place) other;
+
+        return Objects.equals(mLatLng, otherPlace.mLatLng) && Objects.equals(mMarker,
+                otherPlace.mMarker);
+    }
+
+    private Place(Builder builder) {
+        mLatLng = builder.mLatLng;
+        mMarker = builder.mMarker;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private Place() {
+        mLatLng = null;
+        mMarker = null;
+    }
+
+    /** A builder of {@link Place}. */
+    public static final class Builder {
+        private LatLng mLatLng;
+        @Nullable
+        private PlaceMarker mMarker;
+
+        private Builder(LatLng latLng) {
+            this.mLatLng = latLng;
+        }
+
+        private Builder(Place place) {
+            mLatLng = requireNonNull(place.mLatLng);
+            mMarker = place.mMarker;
+        }
+
+        /**
+         * Sets the geographical location associated with this place.
+         *
+         * @throws NullPointerException if {@code latLng} is {@code null}.
+         */
+        @NonNull
+        public Builder setLatLng(@NonNull LatLng latLng) {
+            this.mLatLng = requireNonNull(latLng);
+            return this;
+        }
+
+        /**
+         * Sets the {@link PlaceMarker} that specifies how this place is to be displayed on a
+         * map, or
+         * {@code null} to not display a marker for this place.
+         *
+         * <p>By default and unless otherwise set in this method, a marker will not be displayed.
+         */
+        @NonNull
+        public Builder setMarker(@Nullable PlaceMarker marker) {
+            this.mMarker = marker;
+            return this;
+        }
+
+        /** Constructs the {@link Place} defined by this builder. */
+        @NonNull
+        public Place build() {
+            return new Place(this);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
new file mode 100644
index 0000000..1cec76f
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
@@ -0,0 +1,404 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE;
+
+import static java.util.Objects.requireNonNull;
+
+import android.Manifest.permission;
+import android.content.Context;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.utils.Logger;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A template that displays a map along with a list of places.
+ *
+ * <p>The map can display markers corresponding to the places in the list. See {@link
+ * Builder#setItemList} for details.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in
+ * {@link androidx.car.app.Screen#getTemplate()}, this template is considered a refresh of a
+ * previous one if:
+ *
+ * <ul>
+ *   <li>The template title has not changed, and
+ *   <li>The previous template is in a loading state (see {@link Builder#setLoading}, or the
+ *       number of rows and the string contents (title, texts, not counting spans) of each row
+ *       between the previous and new {@link ItemList}s have not changed.
+ * </ul>
+ */
+public final class PlaceListMapTemplate implements Template {
+    @Keep
+    private final boolean mIsLoading;
+    @Keep
+    private final boolean mShowCurrentLocation;
+    @Keep
+    @Nullable
+    private final CarText mTitle;
+    @Keep
+    @Nullable
+    private final ItemList mItemList;
+    @Keep
+    @Nullable
+    private final Action mHeaderAction;
+    @Keep
+    @Nullable
+    private final ActionStrip mActionStrip;
+    @Keep
+    @Nullable
+    private final Place mAnchor;
+
+    /** Constructs a new builder of {@link PlaceListMapTemplate}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public boolean isCurrentLocationEnabled() {
+        return mShowCurrentLocation;
+    }
+
+    @Nullable
+    public CarText getTitle() {
+        return mTitle;
+    }
+
+    public boolean isLoading() {
+        return mIsLoading;
+    }
+
+    @Nullable
+    public ItemList getItemList() {
+        return mItemList;
+    }
+
+    @Nullable
+    public Action getHeaderAction() {
+        return mHeaderAction;
+    }
+
+    @Nullable
+    public ActionStrip getActionStrip() {
+        return mActionStrip;
+    }
+
+    @Nullable
+    public Place getAnchor() {
+        return mAnchor;
+    }
+
+    @Override
+    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+        requireNonNull(oldTemplate);
+        if (oldTemplate.getClass() != this.getClass()) {
+            return false;
+        }
+
+        PlaceListMapTemplate old = (PlaceListMapTemplate) oldTemplate;
+        if (!Objects.equals(old.getTitle(), getTitle())) {
+            return false;
+        }
+
+        if (old.mIsLoading) {
+            // Transition from a previous loading state is allowed.
+            return true;
+        } else if (mIsLoading) {
+            // Transition to a loading state is disallowed.
+            return false;
+        }
+
+        return requireNonNull(mItemList).isRefresh(old.getItemList(), logger);
+    }
+
+    @Override
+    public void checkPermissions(@NonNull Context context) {
+        if (isCurrentLocationEnabled()) {
+            CarAppPermission.checkHasPermission(context, permission.ACCESS_FINE_LOCATION);
+        }
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "PlaceListMapTemplate";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mShowCurrentLocation, mIsLoading, mTitle, mItemList, mHeaderAction, mActionStrip,
+                mAnchor);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof PlaceListMapTemplate)) {
+            return false;
+        }
+        PlaceListMapTemplate otherTemplate = (PlaceListMapTemplate) other;
+
+        return mShowCurrentLocation == otherTemplate.mShowCurrentLocation
+                && mIsLoading == otherTemplate.mIsLoading
+                && Objects.equals(mTitle, otherTemplate.mTitle)
+                && Objects.equals(mItemList, otherTemplate.mItemList)
+                && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+                && Objects.equals(mActionStrip, otherTemplate.mActionStrip)
+                && Objects.equals(mAnchor, otherTemplate.mAnchor);
+    }
+
+    private PlaceListMapTemplate(Builder builder) {
+        mShowCurrentLocation = builder.mShowCurrentLocation;
+        mIsLoading = builder.mIsLoading;
+        mTitle = builder.mTitle;
+        mItemList = builder.mItemList;
+        mHeaderAction = builder.mHeaderAction;
+        mActionStrip = builder.mActionStrip;
+        mAnchor = builder.mAnchor;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private PlaceListMapTemplate() {
+        mShowCurrentLocation = false;
+        mIsLoading = false;
+        mTitle = null;
+        mItemList = null;
+        mHeaderAction = null;
+        mActionStrip = null;
+        mAnchor = null;
+    }
+
+    /** A builder of {@link PlaceListMapTemplate}. */
+    public static final class Builder {
+        private boolean mShowCurrentLocation;
+        private boolean mIsLoading;
+        @Nullable
+        private CarText mTitle;
+        @Nullable
+        private ItemList mItemList;
+        @Nullable
+        private Action mHeaderAction;
+        @Nullable
+        private ActionStrip mActionStrip;
+        @Nullable
+        private Place mAnchor;
+
+        /**
+         * Sets whether to show the current location in the map.
+         *
+         * <p>The map template will show the user's current location on the map, which is normally
+         * indicated by a blue dot.
+         *
+         * <p>This functionality requires the app to have the {@code ACCESS_FINE_LOCATION}
+         * permission.
+         */
+        @NonNull
+        public Builder setCurrentLocationEnabled(boolean isEnabled) {
+            this.mShowCurrentLocation = isEnabled;
+            return this;
+        }
+
+        /**
+         * Sets whether the template is in a loading state.
+         *
+         * <p>If set to {@code true}, the UI will display a loading indicator where the list content
+         * would be otherwise. The caller is expected to call {@link
+         * androidx.car.app.Screen#invalidate()} and send the new template content
+         * to the host once the data is ready. If set to {@code false}, the UI shows the {@link
+         * ItemList} contents added via {@link #setItemList}.
+         */
+        @NonNull
+        public Builder setLoading(boolean isLoading) {
+            this.mIsLoading = isLoading;
+            return this;
+        }
+
+        /**
+         * Sets the {@link Action} that will be displayed in the header of the template, or
+         * {@code null}
+         * to not display an action.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template only supports either either one of {@link Action#APP_ICON} and {@link
+         * Action#BACK} as a header {@link Action}.
+         *
+         * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setHeaderAction(@Nullable Action headerAction) {
+            ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+                    headerAction == null ? Collections.emptyList()
+                            : Collections.singletonList(headerAction));
+            this.mHeaderAction = headerAction;
+            return this;
+        }
+
+        /**
+         * Sets the {@link CharSequence} to show as the template's title, or {@code null} to not
+         * display
+         * a title.
+         */
+        @NonNull
+        public Builder setTitle(@Nullable CharSequence title) {
+            this.mTitle = title == null ? null : CarText.create(title);
+            return this;
+        }
+
+        /**
+         * Sets an {@link ItemList} to show in a list view along with the map, or {@code null} to
+         * not
+         * display a list.
+         *
+         * <p>To show a marker corresponding to a point of interest represented by a row, set the
+         * {@link
+         * Place} instance via {@link Row.Builder#setMetadata}. The host will display the {@link
+         * PlaceMarker} in both the map and the list view as the row becomes visible.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 6 {@link Row}s in the {@link ItemList}. The host will
+         * ignore any
+         * items over that limit. The list itself cannot be selectable as set via {@link
+         * ItemList.Builder#setSelectable}. Each {@link Row} can add up to 2 lines of texts via
+         * {@link
+         * Row.Builder#addText} and cannot contain a {@link Toggle}.
+         *
+         * <p>Images of type {@link Row#IMAGE_TYPE_LARGE} are not allowed in this template.
+         *
+         * <p>Rows are not allowed to have both and an image and a place marker.
+         *
+         * <p>All non-browsable rows must have a {@link DistanceSpan} attached to either its
+         * title or
+         * texts to indicate the distance of the point of interest from the current location. A
+         * row is
+         * browsable when it's configured like so with {@link Row.Builder#setBrowsable(boolean)}.
+         *
+         * @throws IllegalArgumentException if {@code itemList} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setItemList(@Nullable ItemList itemList) {
+            if (itemList != null) {
+                List<Object> items = itemList.getItems();
+                ROW_LIST_CONSTRAINTS_SIMPLE.validateOrThrow(itemList);
+                ModelUtils.validateAllNonBrowsableRowsHaveDistance(items);
+                ModelUtils.validateAllRowsHaveOnlySmallImages(items);
+                ModelUtils.validateNoRowsHaveBothMarkersAndImages(items);
+            }
+            this.mItemList = itemList;
+
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(LIBRARY)
+        @NonNull
+        public Builder setItemListForTesting(@Nullable ItemList itemList) {
+            this.mItemList = itemList;
+            return this;
+        }
+
+        /**
+         * Sets the {@link ActionStrip} for this template, or {@code null} to not display an {@link
+         * ActionStrip}.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
+         * {@link Action}s, one of them can contain a title as set via
+         * {@link Action.Builder#setTitle}.
+         * Otherwise, only {@link Action}s with icons are allowed.
+         *
+         * @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+            ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+                    actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+            this.mActionStrip = actionStrip;
+            return this;
+        }
+
+        /**
+         * Sets the anchor maker on the map, or {@code null} to not display an anchor marker.
+         *
+         * <p>The anchor marker is displayed differently from other markers by the host.
+         *
+         * <p>If not {@code null}, an anchor marker will be shown at the specified {@link LatLng}
+         * on the
+         * map. The camera will adapt to always have the anchor marker visible within its viewport,
+         * along with other places' markers from {@link Row} that are currently visible in the
+         * {@link
+         * Pane}. This can be used to provide a reference point on the map (e.g. the center of a
+         * search
+         * region) as the user pages through the {@link Pane}'s markers, for example.
+         */
+        @NonNull
+        public Builder setAnchor(@Nullable Place anchor) {
+            this.mAnchor = anchor;
+            return this;
+        }
+
+        /**
+         * Constructs the template defined by this builder.
+         *
+         * <h4>Requirements</h4>
+         *
+         * Either a header {@link Action} or title must be set on the template.
+         *
+         * @throws IllegalArgumentException if the template is in a loading state but the list is
+         *                                  set,
+         *                                  or vice versa.
+         * @throws IllegalStateException    if the template does not have either a title or header
+         *                                  {@link Action} set.
+         */
+        @NonNull
+        public PlaceListMapTemplate build() {
+            boolean hasList = mItemList != null;
+            if (mIsLoading == hasList) {
+                throw new IllegalArgumentException(
+                        "Template is in a loading state but a list is set, or vice versa.");
+            }
+
+            if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+                throw new IllegalStateException("Either the title or header action must be set");
+            }
+
+            return new PlaceListMapTemplate(this);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/PlaceMarker.java b/car/app/app/src/main/java/androidx/car/app/model/PlaceMarker.java
new file mode 100644
index 0000000..173ea25
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/PlaceMarker.java
@@ -0,0 +1,296 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.constraints.CarColorConstraints;
+import androidx.car.app.model.constraints.CarIconConstraints;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/** Describes how a place is to be displayed on a map. */
+public class PlaceMarker {
+    /**
+     * Describes the type of image a marker icon represents.
+     *
+     * @hide
+     */
+    @IntDef(value = {TYPE_ICON, TYPE_IMAGE})
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY)
+    public @interface MarkerIconType {
+    }
+
+    /**
+     * Represents a marker icon.
+     *
+     * <p>Icons always have a tint applied to them.
+     */
+    public static final int TYPE_ICON = 0;
+
+    /**
+     * Represents a marker image.
+     *
+     * <p>No background will be applied.
+     */
+    public static final int TYPE_IMAGE = 1;
+
+    private static final PlaceMarker DEFAULT_INSTANCE = PlaceMarker.builder().build();
+    private static final int MAX_LABEL_LENGTH = 3;
+
+    @Keep
+    @Nullable
+    private final CarIcon mIcon;
+    @Keep
+    @Nullable
+    private final CarText mLabel;
+    @Keep
+    @Nullable
+    private final CarColor mColor;
+    @Keep
+    @MarkerIconType
+    private final int mIconType;
+
+    /**
+     * Returns an instance of {@link PlaceMarker} that uses the default values of the attributes
+     * specified through a {@link Builder}.
+     */
+    @NonNull
+    public static PlaceMarker getDefault() {
+        return DEFAULT_INSTANCE;
+    }
+
+    /**
+     * Returns a {@link Builder} for a {@link PlaceMarker}.
+     */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns {@code true} if {@code marker} is a default marker, {@code false} otherwise.
+     */
+    public static boolean isDefaultMarker(@Nullable PlaceMarker marker) {
+        return marker != null && marker.getIcon() == null && marker.getLabel() == null;
+    }
+
+    /**
+     * Returns the {@link CarIcon} associated with this marker.
+     */
+    @Nullable
+    public CarIcon getIcon() {
+        return mIcon;
+    }
+
+    /**
+     * Returns the type of icon used with this marker.
+     */
+    @MarkerIconType
+    public int getIconType() {
+        return mIconType;
+    }
+
+    /**
+     * If set, the text that should be rendered as the marker's content, {@code null} otherwise.
+     *
+     * <p>Note that a {@link PlaceMarker} can only display either an icon or a text label. If
+     * both are
+     * set, then {@link #getIcon()} will take precedence.
+     */
+    @Nullable
+    public CarText getLabel() {
+        return mLabel;
+    }
+
+    /**
+     * Returns the marker color or {@code null} if not set.
+     *
+     * <p>See {@link Builder#setColor} on rules related to how the color is applied.
+     */
+    @Nullable
+    public CarColor getColor() {
+        return mColor;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "["
+                + (mIcon != null
+                ? mIcon.toString()
+                : mLabel != null ? CarText.toShortString(mLabel) : super.toString())
+                + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mIcon, mLabel, mColor, mIconType);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof PlaceMarker)) {
+            return false;
+        }
+        PlaceMarker otherMarker = (PlaceMarker) other;
+
+        return Objects.equals(mIcon, otherMarker.mIcon)
+                && Objects.equals(mLabel, otherMarker.mLabel)
+                && Objects.equals(mColor, otherMarker.mColor)
+                && mIconType == otherMarker.mIconType;
+    }
+
+    private PlaceMarker(@NonNull Builder builder) {
+        mIcon = builder.mIcon;
+        mIconType = builder.mIconType;
+        mLabel = builder.mLabel;
+        mColor = builder.mColor;
+    }
+
+    /** Private empty constructor used by serialization code. */
+    private PlaceMarker() {
+        mIcon = null;
+        mIconType = TYPE_ICON;
+        mLabel = null;
+        mColor = null;
+    }
+
+    /** A builder of {@link PlaceMarker}. */
+    public static final class Builder {
+        @Nullable
+        private CarIcon mIcon;
+        @Nullable
+        private CarText mLabel;
+        @Nullable
+        private CarColor mColor;
+        @MarkerIconType
+        private int mIconType = TYPE_ICON;
+
+        /**
+         * Sets the icon to display in the marker, or {@code null} to not display one.
+         *
+         * <p>If a label is specified with {@link #setLabel}, the icon will take precedence over it.
+         *
+         * <h4>Icon Sizing Guidance</h4>
+         *
+         * <ul>
+         *   <li>For {@link #TYPE_IMAGE}, the provided image should be 36 x 36 dp. The host
+         *       applies 4 dp rounded corners before the icon is rendered on either the map or
+         *       the list.
+         *   <li>For {@link #TYPE_ICON}, the provided icon should be 32 x 32 dp and have its tint
+         *       value set via {@link CarIcon.Builder#setTint}. Otherwise, a default tint color as
+         *       determined by the host will be applied.
+         * </ul>
+         *
+         * <p>If the size of the provided icon exceeds the size requirements described above in
+         * either one of the dimensions, it will be scaled down and centered inside the bounding
+         * box while preserving the aspect ratio.
+         *
+         * <p>See {@link CarIcon} for more details related to providing icon and image resources
+         * that work with different car screen pixel densities.
+         *
+         * @param icon     the {@link CarIcon} to display inside the marker.
+         * @param iconType one of {@link #TYPE_ICON} or {@link #TYPE_IMAGE}.
+         */
+        @NonNull
+        public Builder setIcon(@Nullable CarIcon icon, @MarkerIconType int iconType) {
+            CarIconConstraints.DEFAULT.validateOrThrow(icon);
+            this.mIcon = icon;
+            this.mIconType = iconType;
+            return this;
+        }
+
+        /**
+         * Sets the text that should be displayed as the marker's content.
+         *
+         * <p>If an icon is specified with {@link #setIcon}, the icon will take precedence.
+         *
+         * @param label the text to display inside of the marker. The string must have a maximum
+         *              size of 3 characters. Set to {@code null} to let the host choose a
+         *              labelling scheme (for example, using a sequence of numbers).
+         */
+        @NonNull
+        public Builder setLabel(@Nullable CharSequence label) {
+            if (label != null && label.length() > MAX_LABEL_LENGTH) {
+                throw new IllegalArgumentException(
+                        "Marker label cannot contain more than " + MAX_LABEL_LENGTH
+                                + " characters");
+            }
+
+            this.mLabel = label == null ? null : CarText.create(label);
+            return this;
+        }
+
+        /**
+         * Sets the color that should be used for the marker on the map.
+         *
+         * <p>This color is applied in the following cases:
+         *
+         * <ul>
+         *   <li>When the {@link PlaceMarker} is displayed on the map, the pin enclosing the icon or
+         *       label will be painted using the given color.
+         *   <li>When the {@link PlaceMarker} is displayed on the list, the color will be applied
+         *       if the content is a label. A label rendered inside a map's pin cannot be color
+         *       and will always use the default color as chosen by the host.
+         * </ul>
+         *
+         * <p>When this is set to {@code null}, the host will use a default color. The host may also
+         * ignore this color and use the default instead if the color does not pass the contrast
+         * requirements.
+         *
+         * <p>A color cannot be set if the marker's icon type is of {@link #TYPE_IMAGE}.
+         */
+        @NonNull
+        public Builder setColor(@Nullable CarColor color) {
+            if (color != null) {
+                CarColorConstraints.UNCONSTRAINED.validateOrThrow(color);
+            }
+            this.mColor = color;
+            return this;
+        }
+
+        /**
+         * Constructs the {@link PlaceMarker} defined by this builder.
+         *
+         * @throws IllegalStateException if the icon is of the type {@link #TYPE_IMAGE} and a a
+         *                               color is set.
+         */
+        @NonNull
+        public PlaceMarker build() {
+            if (mColor != null && (mIcon != null && mIconType == TYPE_IMAGE)) {
+                throw new IllegalStateException("Color cannot be set for icon set with TYPE_IMAGE");
+            }
+
+            return new PlaceMarker(this);
+        }
+
+        private Builder() {
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
new file mode 100644
index 0000000..9379c55
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -0,0 +1,588 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.model.Metadata.EMPTY_METADATA;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.constraints.CarIconConstraints;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a row with a title, several lines of text, an optional image, and an optional action
+ * or switch.
+ */
+public class Row implements Item {
+    /**
+     * Represents flags that control some attributes of the row.
+     *
+     * @hide
+     */
+    // TODO(shiufai): investigate how to expose IntDefs if needed.
+    @RestrictTo(LIBRARY)
+    @IntDef(
+            value = {ROW_FLAG_NONE, ROW_FLAG_SHOW_DIVIDERS, ROW_FLAG_SECTION_HEADER},
+            flag = true)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RowFlags {
+    }
+
+    /**
+     * The type of images supported within rows.
+     *
+     * @hide
+     */
+    // TODO(shiufai): investigate how to expose IntDefs if needed.
+    @RestrictTo(LIBRARY)
+    @IntDef(value = {IMAGE_TYPE_SMALL, IMAGE_TYPE_ICON, IMAGE_TYPE_LARGE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RowImageType {
+    }
+
+    /**
+     * No flags applied to the row.
+     */
+    public static final int ROW_FLAG_NONE = (1 << 0);
+
+    /**
+     * Whether to show dividers around the row.
+     */
+    public static final int ROW_FLAG_SHOW_DIVIDERS = (1 << 1);
+
+    /**
+     * Whether the row is a section header.
+     *
+     * <p>Sections are used to group rows in the UI, for example, by showing them all within a block
+     * of the same background color.
+     *
+     * <p>A section header is a string of text above the section with a title for it.
+     */
+    public static final int ROW_FLAG_SECTION_HEADER = (1 << 2);
+
+    /**
+     * Represents a small image to be displayed in the row.
+     *
+     * <p>If necessary, small images will be scaled down to fit within a 36 x 36 dp bounding box,
+     * preserving their aspect ratio.
+     */
+    public static final int IMAGE_TYPE_SMALL = (1 << 0);
+
+    /**
+     * Represents a large image to be displayed in the row.
+     *
+     * <p>If necessary, large images will be scaled down to fit within a 64 x 64 dp bounding box,
+     * preserving their aspect ratio.
+     */
+    public static final int IMAGE_TYPE_LARGE = (1 << 1);
+
+    /**
+     * Represents a small image to be displayed in the row.
+     *
+     * <p>If necessary, icons will be scaled down to fit within a 44 x 44 dp bounding box,
+     * preserving
+     * their aspect ratios.
+     *
+     * <p>A tint color is expected to be provided via {@link CarIcon.Builder#setTint}. Otherwise, a
+     * default tint color as determined by the host will be applied.
+     */
+    public static final int IMAGE_TYPE_ICON = (1 << 2);
+
+    @Keep
+    @Nullable
+    private final CarText mTitle;
+    @Keep
+    @Nullable
+    private final List<CarText> mTexts;
+    @Keep
+    @Nullable
+    private final CarIcon mImage;
+    @Keep
+    @Nullable
+    private final Toggle mToggle;
+    @Keep
+    @Nullable
+    private final OnClickListenerWrapper mOnClickListener;
+    @Keep
+    private final Metadata mMetadata;
+    @Keep
+    @RowFlags
+    private final int mFlags;
+    @Keep
+    private final boolean mIsBrowsable;
+    @Keep
+    @RowImageType
+    private final int mRowImageType;
+
+    /** Constructs a new builder of {@link Row}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** Returns the title of the row. */
+    @NonNull
+    public CarText getTitle() {
+        return requireNonNull(mTitle);
+    }
+
+    /** Returns the list of text below the title. */
+    @NonNull
+    public List<CarText> getTexts() {
+        Objects.requireNonNull(mTexts);
+        return mTexts;
+    }
+
+    /** Returns the image of the row. */
+    @Nullable
+    public CarIcon getImage() {
+        return mImage;
+    }
+
+    /** Returns the type of the image in the row. */
+    @RowImageType
+    public int getRowImageType() {
+        return mRowImageType;
+    }
+
+    /**
+     * Returns the {@link Toggle} in the row or {@code null} if the row does not contain a
+     * toggle.
+     */
+    @Nullable
+    public Toggle getToggle() {
+        return mToggle;
+    }
+
+    /**
+     * Returns {@code true} if the row is browsable, {@code false} otherwise.
+     *
+     * <p>If a row is browsable, then no {@link Action} or {@link Toggle} can be added to it.
+     */
+    public boolean isBrowsable() {
+        return mIsBrowsable;
+    }
+
+    /**
+     * Returns the {@link OnClickListener} to be called back when the row is clicked, or {@code
+     * null} if the row is non-clickable.
+     */
+    @Nullable
+    public OnClickListenerWrapper getOnClickListener() {
+        return mOnClickListener;
+    }
+
+    /**
+     * Returns the {@link Metadata} associated with the row.
+     */
+    @NonNull
+    public Metadata getMetadata() {
+        return mMetadata;
+    }
+
+    /**
+     * Returns the flags for the row.
+     */
+    @RowFlags
+    public int getFlags() {
+        return mFlags;
+    }
+
+    /**
+     * Rows your boat.
+     *
+     * <p>Example usage:
+     *
+     * <pre>{@code
+     * row.row().row().yourBoat(); // gently down the stream
+     * }</pre>
+     */
+    public void yourBoat() {
+    }
+
+    /** Returns a {@link Row} for rowing {@link #yourBoat()} */
+    @NonNull
+    public Row row() {
+        return this;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[title: "
+                + CarText.toShortString(mTitle)
+                + ", text count: "
+                + (mTexts != null ? mTexts.size() : 0)
+                + ", image: "
+                + mImage
+                + ", isBrowsable: "
+                + mIsBrowsable
+                + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mTitle,
+                mTexts,
+                mImage,
+                mToggle,
+                mOnClickListener == null,
+                mMetadata,
+                mFlags,
+                mIsBrowsable,
+                mRowImageType);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof Row)) {
+            return false;
+        }
+        Row otherRow = (Row) other;
+
+        // Don't compare listener, only the fact whether it's present.
+        return Objects.equals(mTitle, otherRow.mTitle)
+                && Objects.equals(mTexts, otherRow.mTexts)
+                && Objects.equals(mImage, otherRow.mImage)
+                && Objects.equals(mToggle, otherRow.mToggle)
+                && Objects.equals(mOnClickListener == null, otherRow.mOnClickListener == null)
+                && Objects.equals(mMetadata, otherRow.mMetadata)
+                && mFlags == otherRow.mFlags
+                && mIsBrowsable == otherRow.mIsBrowsable
+                && mRowImageType == otherRow.mRowImageType;
+    }
+
+    private Row(Builder builder) {
+        mTitle = builder.mTitle;
+        mTexts = new ArrayList<>(builder.mTexts);
+        mImage = builder.mImage;
+        mToggle = builder.mToggle;
+        mOnClickListener = builder.mOnClickListener;
+        mMetadata = builder.mMetadata;
+        mIsBrowsable = builder.mIsBrowsable;
+        mFlags = builder.mFlags;
+        mRowImageType = builder.mRowImageType;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private Row() {
+        mTitle = null;
+        mTexts = null;
+        mImage = null;
+        mToggle = null;
+        mOnClickListener = null;
+        mMetadata = EMPTY_METADATA;
+        mIsBrowsable = false;
+        mFlags = ROW_FLAG_NONE;
+        mRowImageType = IMAGE_TYPE_SMALL;
+    }
+
+    /** A builder of {@link Row}. */
+    public static final class Builder {
+        @Nullable
+        private CarText mTitle;
+        private final List<CarText> mTexts = new ArrayList<>();
+        @Nullable
+        private CarIcon mImage;
+        @Nullable
+        private Toggle mToggle;
+        @Nullable
+        private OnClickListenerWrapper mOnClickListener;
+        private Metadata mMetadata = EMPTY_METADATA;
+        private boolean mIsBrowsable;
+        @RowFlags
+        private int mFlags = ROW_FLAG_NONE;
+        @RowImageType
+        private int mRowImageType = IMAGE_TYPE_SMALL;
+
+        /**
+         * Sets the title of the row.
+         *
+         * @throws NullPointerException     if {@code title} is {@code null}.
+         * @throws IllegalArgumentException if {@code title} is empty.
+         */
+        @NonNull
+        public Builder setTitle(@NonNull CharSequence title) {
+            CarText titleText = CarText.create(requireNonNull(title));
+            if (titleText.isEmpty()) {
+                throw new IllegalArgumentException("The title cannot be null or empty");
+            }
+            this.mTitle = titleText;
+            return this;
+        }
+
+        /**
+         * Sets the title of the row, or {@code null} to not show a title.
+         *
+         * @hide
+         */
+        @RestrictTo(LIBRARY)
+        @NonNull
+        public Builder setTitle(@Nullable CarText title) {
+            this.mTitle = title;
+            return this;
+        }
+
+        /**
+         * Adds a text string to the row below the title.
+         *
+         * <p>The text's color can be customized with {@link ForegroundCarColorSpan} instances.
+         *
+         * <p>Most templates allow up to 2 text strings, but this may vary. This limit is
+         * documented in each individual template.
+         *
+         * <h4>Text Wrapping</h4>
+         *
+         * Each string added with {@link #addText} will not wrap more than 1 line in the UI, with
+         * one exception: if the template allows a maximum number of text strings larger than 1, and
+         * the app adds a single text string, then this string will wrap up to the maximum.
+         *
+         * <p>For example, assuming 2 lines are allowed in the template where the row will be
+         * used, this code:
+         *
+         * <pre>{@code
+         * rowBuilder
+         *     .addText("This is a rather long line of text")
+         *     .addText("More text")
+         * }</pre>
+         *
+         * <p>would wrap the text like this:
+         *
+         * <pre>
+         * This is a rather long li...
+         * More text
+         * </pre>
+         *
+         * In contrast, this code:
+         *
+         * <pre>{@code
+         * rowBuilder
+         *     .addText("This is a rather long line of text. More text")
+         * }</pre>
+         *
+         * <p>would wrap the single line of text at a maximum of 2 lines, producing a different
+         * result:
+         *
+         * <pre>
+         * This is a rather long line
+         * of text. More text
+         * </pre>
+         *
+         * <p>Note that when using a single line, a line break character can be used to break it
+         * into two, but the results may be unpredictable depending on the width the text is
+         * wrapped at:
+         *
+         * <pre>{@code
+         * rowBuilder
+         *     .addText("This is a rather long line of text\nMore text")
+         * }</pre>
+         *
+         * <p>would produce a result that may loose the "More text" string:
+         *
+         * <pre>
+         * This is a rather long line
+         * of text
+         * </pre>
+         *
+         * @throws NullPointerException if {@code text} is {@code null}.
+         * @see ForegroundCarColorSpan
+         */
+        @NonNull
+        public Builder addText(@NonNull CharSequence text) {
+            this.mTexts.add(CarText.create(requireNonNull(text)));
+            return this;
+        }
+
+        /**
+         * Clears any rows that may have been added with {@link #addText(CharSequence)} up to this
+         * point.
+         */
+        @NonNull
+        public Builder clearText() {
+            mTexts.clear();
+            return this;
+        }
+
+        /**
+         * Adds a line text of the row below the title.
+         *
+         * @throws NullPointerException if {@code text} is {@code null}.
+         * @hide
+         */
+        @RestrictTo(LIBRARY)
+        @NonNull
+        public Builder addText(@NonNull CarText text) {
+            this.mTexts.add(requireNonNull(text));
+            return this;
+        }
+
+        /**
+         * Sets an image to show in the row with the default size {@link #IMAGE_TYPE_SMALL}, or
+         * {@code null} to not display an image in the row.
+         *
+         * @see #setImage(CarIcon, int)
+         */
+        @NonNull
+        public Builder setImage(@Nullable CarIcon image) {
+            return setImage(image, IMAGE_TYPE_SMALL);
+        }
+
+        /**
+         * Sets an image to show in the row with the given image type, or {@code null} to not
+         * display an image in the row.
+         *
+         * <p>For a custom {@link CarIcon}, its {@link androidx.core.graphics.drawable.IconCompat}
+         * instance can be of {@link androidx.core.graphics.drawable.IconCompat#TYPE_BITMAP},
+         * {@link androidx.core.graphics.drawable.IconCompat#TYPE_RESOURCE}, or
+         * {@link androidx.core.graphics.drawable.IconCompat#TYPE_URI}.
+         *
+         * <h4>Image Sizing Guidance</h4>
+         *
+         * <p>If the input image's size exceeds the sizing requirements for the given image type in
+         * either one of the dimensions, it will be scaled down to be centered inside the
+         * bounding box while preserving the aspect ratio.
+         *
+         * <p>See {@link CarIcon} for more details related to providing icon and image resources
+         * that work with different car screen pixel densities.
+         *
+         * @param image     the {@link CarIcon} to display, or {@code null} to not display one.
+         * @param imageType one of {@link #IMAGE_TYPE_ICON}, {@link #IMAGE_TYPE_SMALL} or {@link
+         *                  #IMAGE_TYPE_LARGE}
+         */
+        @NonNull
+        public Builder setImage(@Nullable CarIcon image, @RowImageType int imageType) {
+            CarIconConstraints.UNCONSTRAINED.validateOrThrow(image);
+            this.mImage = image;
+            this.mRowImageType = imageType;
+            return this;
+        }
+
+        /**
+         * Sets a {@link Toggle} to show in the row, or {@code null} to not display a toggle in
+         * the row.
+         */
+        @NonNull
+        public Builder setToggle(@Nullable Toggle toggle) {
+            this.mToggle = toggle;
+            return this;
+        }
+
+        /**
+         * Shows an icon at the end of the row that indicates that the row is browsable.
+         *
+         * <p>Browsable rows can be used, for example, to represent the parent row in a hierarchy of
+         * lists with child lists.
+         *
+         * <p>If a row is browsable, then no {@link Action} or {@link Toggle} can be added to it.
+         */
+        @NonNull
+        public Builder setBrowsable(boolean isBrowsable) {
+            this.mIsBrowsable = isBrowsable;
+            return this;
+        }
+
+        /**
+         * Sets the {@link OnClickListener} to be called back when the row is clicked, or {@code
+         * null} to make the row non-clickable.
+         */
+        @NonNull
+        @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+        public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
+            if ( null) {
+                this.mOnClickListener = null;
+            } else {
+                this.mOnClickListener = OnClickListenerWrapper.create(onClickListener);
+            }
+            return this;
+        }
+
+        /**
+         * Sets the {@link Metadata} associated with the row.
+         *
+         * @param metadata The metadata to set with the row. Pass {@link Metadata#EMPTY_METADATA}
+         *                 to not associate any metadata with the row.
+         */
+        @NonNull
+        public Builder setMetadata(@NonNull Metadata metadata) {
+            this.mMetadata = metadata;
+            return this;
+        }
+
+        /**
+         * Sets flags for the row.
+         */
+        @NonNull
+        public Builder setFlags(@RowFlags int flags) {
+            this.mFlags = flags;
+            return this;
+        }
+
+        /**
+         * Constructs the {@link Row} defined by this builder.
+         *
+         * @throws IllegalStateException if the row's title is not set.
+         * @throws IllegalStateException if the row is a browsable row and has a {@link Toggle}.
+         * @throws IllegalStateException if the row is a browsable row but does not have a {@link
+         *                               OnClickListener}.
+         * @throws IllegalStateException if the row has both a {@link OnClickListener} and a {@link
+         *                               Toggle}.
+         */
+        @NonNull
+        public Row build() {
+            if (mTitle == null) {
+                throw new IllegalStateException("A title must be set on the row");
+            }
+
+            if (mIsBrowsable) {
+                if (mToggle != null) {
+                    throw new IllegalStateException("A browsable row must not have a toggle set");
+                }
+                if (mOnClickListener == null) {
+                    throw new IllegalStateException(
+                            "A browsable row must have its onClickListener set");
+                }
+            }
+
+            if (mToggle != null && mOnClickListener != null) {
+                throw new IllegalStateException(
+                        "If a row contains a toggle, it must not have a onClickListener set");
+            }
+
+            return new Row(this);
+        }
+
+        private Builder() {
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java
new file mode 100644
index 0000000..b2048b4
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java
@@ -0,0 +1,415 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.ISearchListener;
+import androidx.car.app.Screen;
+import androidx.car.app.SearchListener;
+import androidx.car.app.utils.Logger;
+import androidx.car.app.utils.RemoteUtils;
+
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * A model that allows the user to enter text searches, and can display results in a list.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in {@link Screen#getTemplate()}, this template
+ * supports any content changes as refreshes. This allows apps to interactively update the search
+ * results as the user types without the templates being counted against the quota.
+ */
+public final class SearchTemplate implements Template {
+    @Keep
+    private final boolean mIsLoading;
+    @Keep
+    private final ISearchListener mSearchListener;
+    @Keep
+    @Nullable
+    private final String mInitialSearchText;
+    @Keep
+    @Nullable
+    private final String mSearchHint;
+    @Keep
+    @Nullable
+    private final ItemList mItemList;
+    @Keep
+    private final boolean mShowKeyboardByDefault;
+    @Keep
+    @Nullable
+    private final Action mHeaderAction;
+    @Keep
+    @Nullable
+    private final ActionStrip mActionStrip;
+
+    /**
+     * Constructs a new builder of {@link SearchTemplate}.
+     *
+     * @param listener the listener to be invoked for events such as when the user types new
+     *                 text, or
+     *                 submits a search.
+     */
+    @NonNull
+    @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+    public static Builder builder(@NonNull SearchListener listener) {
+        return new Builder(listener);
+    }
+
+    public boolean isLoading() {
+        return mIsLoading;
+    }
+
+    @Nullable
+    public Action getHeaderAction() {
+        return mHeaderAction;
+    }
+
+    /**
+     * Returns the {@link ActionStrip} instance set in the template.
+     */
+    @Nullable
+    public ActionStrip getActionStrip() {
+        return mActionStrip;
+    }
+
+    /**
+     * Returns the optional initial search text.
+     *
+     * @see Builder#setInitialSearchText
+     */
+    @Nullable
+    public String getInitialSearchText() {
+        return mInitialSearchText;
+    }
+
+    /**
+     * Returns the optional search hint.
+     *
+     * @see Builder#setSearchHint
+     */
+    @Nullable
+    public String getSearchHint() {
+        return mSearchHint;
+    }
+
+    /**
+     * Returns the optional {@link ItemList} for search results.
+     *
+     * @see Builder#getItemList
+     */
+    @Nullable
+    public ItemList getItemList() {
+        return mItemList;
+    }
+
+    /**
+     * Returns the {@link SearchListener} for search callbacks.
+     *
+     * @hide
+     */
+    // TODO(shiufai): re-surface this API with a wrapper around the AIDL class.
+    @RestrictTo(LIBRARY)
+    @NonNull
+    public ISearchListener getSearchListener() {
+        return mSearchListener;
+    }
+
+    /**
+     * Returns whether to show the keyboard by default.
+     *
+     * @see Builder#setShowKeyboardByDefault
+     */
+    public boolean isShowKeyboardByDefault() {
+        return mShowKeyboardByDefault;
+    }
+
+    @Override
+    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+        // Always allow updating on search templates. Search results needs to be updated on the fly
+        // as user searches.
+        return oldTemplate.getClass() == this.getClass();
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "SearchTemplate";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mInitialSearchText,
+                mIsLoading,
+                mSearchHint,
+                mItemList,
+                mShowKeyboardByDefault,
+                mHeaderAction,
+                mActionStrip);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof SearchTemplate)) {
+            return false;
+        }
+        SearchTemplate otherTemplate = (SearchTemplate) other;
+
+        // Don't compare listener.
+        return mIsLoading == otherTemplate.mIsLoading
+                && Objects.equals(mInitialSearchText, otherTemplate.mInitialSearchText)
+                && Objects.equals(mSearchHint, otherTemplate.mSearchHint)
+                && Objects.equals(mItemList, otherTemplate.mItemList)
+                && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+                && Objects.equals(mActionStrip, otherTemplate.mActionStrip)
+                && mShowKeyboardByDefault == otherTemplate.mShowKeyboardByDefault;
+    }
+
+    private SearchTemplate(Builder builder) {
+        mInitialSearchText = builder.mInitialSearchText;
+        mSearchHint = builder.mSearchHint;
+        mIsLoading = builder.mIsLoading;
+        mItemList = builder.mItemList;
+        mSearchListener = builder.mSearchListener;
+        mShowKeyboardByDefault = builder.mShowKeyboardByDefault;
+        mHeaderAction = builder.mHeaderAction;
+        mActionStrip = builder.mActionStrip;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private SearchTemplate() {
+        mInitialSearchText = null;
+        mSearchHint = null;
+        mIsLoading = false;
+        mItemList = null;
+        mHeaderAction = null;
+        mActionStrip = null;
+        mSearchListener = new SearchListenerStub(
+                new SearchListener() {
+                    @Override
+                    public void onSearchTextChanged(@NonNull String searchText) {
+                    }
+
+                    @Override
+                    public void onSearchSubmitted(@NonNull String searchText) {
+                    }
+                });
+        mShowKeyboardByDefault = true;
+    }
+
+    /** A builder of {@link SearchTemplate}. */
+    public static final class Builder {
+        private final ISearchListener mSearchListener;
+        @Nullable
+        private String mInitialSearchText;
+        @Nullable
+        private String mSearchHint;
+        private boolean mIsLoading;
+        @Nullable
+        private ItemList mItemList;
+        private boolean mShowKeyboardByDefault = true;
+        @Nullable
+        private Action mHeaderAction;
+        @Nullable
+        private ActionStrip mActionStrip;
+
+        private Builder(SearchListener listener) {
+            mSearchListener = new SearchListenerStub(listener);
+        }
+
+        /**
+         * Sets the {@link Action} that will be displayed in the header of the template, or
+         * {@code null}
+         * to not display an action.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template only supports either either one of {@link Action#APP_ICON} and {@link
+         * Action#BACK} as a header {@link Action}.
+         *
+         * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setHeaderAction(@Nullable Action headerAction) {
+            ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+                    headerAction == null ? Collections.emptyList()
+                            : Collections.singletonList(headerAction));
+            this.mHeaderAction = headerAction;
+            return this;
+        }
+
+        /**
+         * Sets the {@link ActionStrip} for this template, or {@code null} to not display an {@link
+         * ActionStrip}.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
+         * {@link Action}s, one of them can contain a title as set via
+         * {@link Action.Builder#setTitle}. Otherwise, only {@link Action}s with icons are allowed.
+         *
+         * @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+            ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+                    actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+            this.mActionStrip = actionStrip;
+            return this;
+        }
+
+        /**
+         * Sets the initial search text to display in the search box, or {@code null} to not
+         * display any initial search text.
+         *
+         * <p>Defaults to {@code null}.
+         */
+        @NonNull
+        public Builder setInitialSearchText(@Nullable String initialSearchText) {
+            this.mInitialSearchText = initialSearchText;
+            return this;
+        }
+
+        /**
+         * Sets the text hint to display in the search box when it is empty, or {@code null} to
+         * use a default search hint.
+         *
+         * <p>This is not the actual search text, and will disappear if user types any value into
+         * the search.
+         *
+         * <p>If a non empty text is set via {@link #setInitialSearchText}, the {@code searchHint
+         * } will not show, unless the user erases the search text.
+         *
+         * <p>Defaults to {@code null}.
+         */
+        @NonNull
+        public Builder setSearchHint(@Nullable String searchHint) {
+            this.mSearchHint = searchHint;
+            return this;
+        }
+
+        /**
+         * Sets whether the template is in a loading state.
+         *
+         * <p>If set to {@code true}, the UI will display a loading indicator where the list content
+         * would be otherwise. The caller is expected to call {@link
+         * androidx.car.app.Screen#invalidate()} and send the new template content
+         * to the host once the data is ready. If set to {@code false}, the UI shows the {@link
+         * ItemList} contents added via {@link #setItemList}.
+         */
+        @NonNull
+        public Builder setLoading(boolean isLoading) {
+            this.mIsLoading = isLoading;
+            return this;
+        }
+
+        /**
+         * Sets the {@link ItemList} to show for search results, or {@code null} if there are no
+         * results.
+         *
+         * <p>The list will be shown below the search box, allowing users to click on individual
+         * search results.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 6 {@link Row}s in the {@link ItemList}. The host will
+         * ignore any items over that limit. The list itself cannot be selectable as set via {@link
+         * ItemList.Builder#setSelectable}. Each {@link Row} can add up to 2 lines of texts via
+         * {@link Row.Builder#addText} and cannot contain a {@link Toggle}.
+         *
+         * @throws IllegalArgumentException if {@code itemList} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setItemList(@Nullable ItemList itemList) {
+            if (itemList != null) {
+                ROW_LIST_CONSTRAINTS_SIMPLE.validateOrThrow(itemList);
+            }
+
+            this.mItemList = itemList;
+            return this;
+        }
+
+        /**
+         * Sets if the keyboard should be displayed by default, instead of waiting until user
+         * interacts with the search box.
+         *
+         * <p>Defaults to {@code true}.
+         */
+        @NonNull
+        public Builder setShowKeyboardByDefault(boolean showKeyboardByDefault) {
+            this.mShowKeyboardByDefault = showKeyboardByDefault;
+            return this;
+        }
+
+        /**
+         * Constructs the {@link SearchTemplate} model.
+         *
+         * @throws IllegalArgumentException if the template is in a loading state but the list is
+         *                                  set.
+         */
+        @NonNull
+        public SearchTemplate build() {
+            if (mIsLoading && mItemList != null) {
+                throw new IllegalArgumentException(
+                        "Template is in a loading state but a list is set.");
+            }
+
+            return new SearchTemplate(this);
+        }
+    }
+
+    @Keep // We need to keep these stub for Bundler serialization logic.
+    private static class SearchListenerStub extends ISearchListener.Stub {
+        private final SearchListener mSearchListener;
+
+        private SearchListenerStub(SearchListener searchListener) {
+            mSearchListener = searchListener;
+        }
+
+        @Override
+        public void onSearchTextChanged(String text, IOnDoneCallback callback) {
+            RemoteUtils.dispatchHostCall(
+                    () -> mSearchListener.onSearchTextChanged(text), callback,
+                    "onSearchTextChanged");
+        }
+
+        @Override
+        public void onSearchSubmitted(String text, IOnDoneCallback callback) {
+            RemoteUtils.dispatchHostCall(
+                    () -> mSearchListener.onSearchSubmitted(text), callback, "onSearchSubmitted");
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/SectionedItemList.java b/car/app/app/src/main/java/androidx/car/app/model/SectionedItemList.java
new file mode 100644
index 0000000..b9ce5b9
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/SectionedItemList.java
@@ -0,0 +1,95 @@
+/*
+ * 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.car.app.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Represents an {@link ItemList} that is contained inside a section, for internal use only.
+ */
+public class SectionedItemList {
+    @Keep
+    @Nullable
+    private final ItemList mItemList;
+    @Keep
+    @Nullable
+    private final CarText mHeader;
+
+    /**
+     * Creates an instance of a {@link SectionedItemList} with the given {@code itemList} and
+     * {@code sectionHeader}.
+     */
+    @NonNull
+    public static SectionedItemList create(
+            @NonNull ItemList itemList, @NonNull CarText sectionHeader) {
+        return new SectionedItemList(requireNonNull(itemList), requireNonNull(sectionHeader));
+    }
+
+    /** Returns the {@link ItemList} for the section. */
+    @NonNull
+    public ItemList getItemList() {
+        return requireNonNull(mItemList);
+    }
+
+    /** Returns the title of the section. */
+    @NonNull
+    public CarText getHeader() {
+        return requireNonNull(mHeader);
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[ items: " + mItemList + ", has header: " + (mHeader != null) + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mItemList, mHeader);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof SectionedItemList)) {
+            return false;
+        }
+        SectionedItemList otherList = (SectionedItemList) other;
+
+        return Objects.equals(mItemList, otherList.mItemList) && Objects.equals(mHeader,
+                otherList.mHeader);
+    }
+
+    private SectionedItemList(@Nullable ItemList itemList, @Nullable CarText header) {
+        this.mItemList = itemList;
+        this.mHeader = header;
+    }
+
+    /** For serialization. */
+    private SectionedItemList() {
+        this.mItemList = null;
+        this.mHeader = null;
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Template.java b/car/app/app/src/main/java/androidx/car/app/model/Template.java
new file mode 100644
index 0000000..46eb37a
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Template.java
@@ -0,0 +1,42 @@
+/*
+ * 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.car.app.model;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.utils.Logger;
+
+/** An interface used to denote a model that can act as a root for a tree of other models. */
+public interface Template {
+
+    /**
+     * Returns {@code true} if this {@link Template} instance is determined to be a refresh compared
+     * to the input template.
+     */
+    default boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+        return false;
+    }
+
+    /**
+     * Checks that the application has the required permissions for this template.
+     *
+     * @throws SecurityException if the application is missing any required permission.
+     */
+    default void checkPermissions(@NonNull Context context) {
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/TemplateInfo.java b/car/app/app/src/main/java/androidx/car/app/model/TemplateInfo.java
new file mode 100644
index 0000000..7084bba
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/TemplateInfo.java
@@ -0,0 +1,77 @@
+/*
+ * 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.car.app.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Stores information about {@link Template} returned from a {@link
+ * androidx.car.app.Screen}.
+ */
+public final class TemplateInfo {
+    @Keep
+    @Nullable
+    private final Class<? extends Template> mTemplateClass;
+    @Keep
+    @Nullable
+    private final String mTemplateId;
+
+    public TemplateInfo(@NonNull Template template, @NonNull String templateId) {
+        this.mTemplateClass = template.getClass();
+        this.mTemplateId = templateId;
+    }
+
+    private TemplateInfo() {
+        this.mTemplateClass = null;
+        this.mTemplateId = null;
+    }
+
+    @NonNull
+    public Class<? extends Template> getTemplateClass() {
+        return requireNonNull(mTemplateClass);
+    }
+
+    @NonNull
+    public String getTemplateId() {
+        return requireNonNull(mTemplateId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mTemplateClass, mTemplateId);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof TemplateInfo)) {
+            return false;
+        }
+        TemplateInfo otherInfo = (TemplateInfo) other;
+
+        return Objects.equals(mTemplateClass, otherInfo.mTemplateClass)
+                && Objects.equals(mTemplateId, otherInfo.mTemplateId);
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/TemplateWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/TemplateWrapper.java
new file mode 100644
index 0000000..a26e151
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/TemplateWrapper.java
@@ -0,0 +1,211 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * A wrapper for mapping a {@link Template} with a unique ID used for implementing task flow
+ * restrictions.
+ *
+ * <p>This is what is sent to the host, so that the host can determine whether the template is a new
+ * template (e.g. a step counts toward the task limit), or an existing template update (e.g. a
+ * refresh that does not count towards the task limit), by checking whether the ID have changed.
+ */
+public final class TemplateWrapper {
+    @Keep
+    private Template mTemplate;
+    @Keep
+    private String mId;
+    @Keep
+    private List<TemplateInfo> mTemplateInfoForScreenStack = new ArrayList<>();
+
+    /** The current step in a task that the template is in. For internal, host-side use only. */
+    private int mCurrentTaskStep;
+
+    /**
+     * Whether the template wrapper is a refresh of the current template. For internal, host-side
+     * use
+     * only.
+     */
+    private boolean mIsRefresh;
+
+    /**
+     * Creates a {@link TemplateWrapper} instance with the given {@link Template}.
+     *
+     * <p>The host will treat the {@link Template} as a new task step, unless it determines through
+     * its internal logic that the {@link Template} is a refresh of the existing view, in which case
+     * the task step will remain the same.
+     */
+    @NonNull
+    public static TemplateWrapper wrap(@NonNull Template template) {
+        // Assign a random ID to the template. This should be unique so that the host knows the
+        // template
+        // is a new step. We are not using hashCode() here as we might override template's hash
+        // codes in
+        // the future.
+        //
+        // Note: There is a chance of collision here, in which case the host will reset the
+        // task step to the value of a previous template that has the colliding ID. The chance of
+        // this
+        // happening should be negligible given we are dealing with a very small number of
+        // templates in
+        // the stack.
+        return wrap(template, createRandomId());
+    }
+
+    /**
+     * Creates a {@link TemplateWrapper} instance with the given {@link Template} and ID.
+     *
+     * <p>The ID is primarily used to inform the host that the given {@link Template} shares the
+     * same
+     * ID as a previously sent {@link Template}, even though their contents differ. In such
+     * cases, the
+     * host will reset the task step to where the previous {@link Template} was.
+     *
+     * <p>For example, the client sends Template A (task step 1), then move forwards a screen and
+     * sends Template B (task step 2). Now the client pops the screen and sends Template C. By
+     * assigning the ID of Template A to Template C, the client library informs the host that it
+     * is a back operation and the task step should be set to 1 again.
+     */
+    @NonNull
+    public static TemplateWrapper wrap(@NonNull Template template, @NonNull String id) {
+        return new TemplateWrapper(requireNonNull(template), requireNonNull(id));
+    }
+
+    /** Returns the wrapped {@link Template}. */
+    @NonNull
+    public Template getTemplate() {
+        return requireNonNull(mTemplate);
+    }
+
+    /** Returns the ID associated with the wrapped {@link Template}. */
+    @NonNull
+    public String getId() {
+        return mId;
+    }
+
+    /**
+     * Sets the {@link TemplateInfo} of each of the last known templates for each of the screens in
+     * the stack managed by the screen manager.
+     *
+     * @hide
+     * @see #getTemplateInfosForScreenStack
+     */
+    @RestrictTo(LIBRARY)
+    public void setTemplateInfosForScreenStack(
+            @NonNull List<TemplateInfo> templateInfoForScreenStack) {
+        this.mTemplateInfoForScreenStack = templateInfoForScreenStack;
+    }
+
+    /**
+     * Returns a {@link TemplateInfo} for the last returned template for each of the screens in the
+     * screen stack managed by the screen manager.
+     *
+     * <p>The return values are in order, where position 0 is the top of the stack, and position
+     * n is
+     * the bottom of the stack given n screens on the stack.
+     */
+    @Nullable
+    public List<TemplateInfo> getTemplateInfosForScreenStack() {
+        return mTemplateInfoForScreenStack;
+    }
+
+    /**
+     * Retrieves the current task step that the template is in. For internal, host-side use only.
+     */
+    public int getCurrentTaskStep() {
+        return mCurrentTaskStep;
+    }
+
+    /**
+     * Sets the current task step that the template is in. For internal, host-side use only.
+     */
+    public void setCurrentTaskStep(int currentTaskStep) {
+        this.mCurrentTaskStep = currentTaskStep;
+    }
+
+    /** Sets whether the template is a refresh of the current template. */
+    public void setRefresh(boolean isRefresh) {
+        this.mIsRefresh = isRefresh;
+    }
+
+    /** Returns {@code true} if the template is a refresh for the previous template. */
+    public boolean isRefresh() {
+        return mIsRefresh;
+    }
+
+    /**
+     * Updates the {@link Template} this {@link TemplateWrapper} instance wraps. For internal,
+     * host-side use only.
+     */
+    public void setTemplate(@NonNull Template template) {
+        this.mTemplate = template;
+    }
+
+    /**
+     * Updates the ID associated with the wrapped {@link Template}. For internal, host-side use
+     * only.
+     */
+    public void setId(@NonNull String id) {
+        this.mId = id;
+    }
+
+    /** Creates a copy of the given {@link TemplateWrapper}. */
+    @NonNull
+    public static TemplateWrapper copyOf(@NonNull TemplateWrapper source) {
+        TemplateWrapper destination = TemplateWrapper.wrap(source.getTemplate(), source.getId());
+        destination.setRefresh(source.isRefresh());
+        destination.setCurrentTaskStep(source.getCurrentTaskStep());
+        List<TemplateInfo> templateInfos = source.getTemplateInfosForScreenStack();
+        if (templateInfos != null) {
+            destination.setTemplateInfosForScreenStack(templateInfos);
+        }
+        return destination;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "[template: " + mTemplate + ", ID: " + mId + "]";
+    }
+
+    private TemplateWrapper(Template template, String id) {
+        this.mTemplate = template;
+        this.mId = id;
+    }
+
+    private TemplateWrapper() {
+        mTemplate = null;
+        mId = "";
+    }
+
+    private static String createRandomId() {
+        return UUID.randomUUID().toString();
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Toggle.java b/car/app/app/src/main/java/androidx/car/app/model/Toggle.java
new file mode 100644
index 0000000..b7b849e
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Toggle.java
@@ -0,0 +1,174 @@
+/*
+ * 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.car.app.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.IOnCheckedChangeListener;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.utils.RemoteUtils;
+
+/** Represents a toggle that can have either a checked or unchecked state. */
+public class Toggle {
+    /** A listener for handling checked state change events. */
+    public interface OnCheckedChangeListener {
+        /** Notifies that the checked state has changed. */
+        void onCheckedChange(boolean isChecked);
+    }
+
+    @Keep
+    @Nullable
+    private final IOnCheckedChangeListener mOnCheckedChangeListener;
+    @Keep
+    private final boolean mIsChecked;
+
+    /**
+     * Constructs a new builder of {@link Toggle}.
+     *
+     * @throws NullPointerException if {@code onCheckedChangeListener} is {@code null}.
+     */
+    @NonNull
+    @SuppressLint("ExecutorRegistration") // this listener is for transport to the host only.
+    public static Builder builder(@NonNull OnCheckedChangeListener onCheckedChangeListener) {
+        return new Builder(requireNonNull(onCheckedChangeListener));
+    }
+
+    /**
+     * Returns {@code true} if the toggle is checked.
+     */
+    public boolean isChecked() {
+        return mIsChecked;
+    }
+
+    /**
+     * Returns the {@link OnCheckedChangeListener} that is called when the checked state of the
+     * {@link Toggle}is changed.
+     *
+     * @hide
+     */
+    // TODO(shiufai): re-surface this API with a wrapper around the AIDL class.
+    @RestrictTo(LIBRARY)
+    @NonNull
+    public IOnCheckedChangeListener getOnCheckedChangeListener() {
+        return requireNonNull(mOnCheckedChangeListener);
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[ isChecked: " + mIsChecked + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Boolean.valueOf(mIsChecked).hashCode();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof Toggle)) {
+            return false;
+        }
+        Toggle otherToggle = (Toggle) other;
+
+        // Don't compare listener.
+        return mIsChecked == otherToggle.mIsChecked;
+    }
+
+    private Toggle(Builder builder) {
+        mIsChecked = builder.mIsChecked;
+        mOnCheckedChangeListener = builder.mOnCheckedChangeListener;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private Toggle() {
+        mOnCheckedChangeListener = null;
+        mIsChecked = false;
+    }
+
+    /** A builder of {@link Toggle}. */
+    public static final class Builder {
+        private IOnCheckedChangeListener mOnCheckedChangeListener;
+        private boolean mIsChecked;
+
+        /**
+         * Sets the initial checked state for {@link Toggle}.
+         *
+         * <p>The default state of a {@link Toggle} is unchecked.
+         */
+        @NonNull
+        public Builder setChecked(boolean checked) {
+            this.mIsChecked = checked;
+            return this;
+        }
+
+        /**
+         * Sets the {@link OnCheckedChangeListener} to call when the checked state of the
+         * {@link Toggle}
+         * is changed.
+         *
+         * @throws NullPointerException if {@code onCheckedChangeListener} is {@code null}.
+         */
+        @NonNull
+        // TODO(shiufai): remove MissingGetterMatchingBuilder once listener is properly exposed.
+        @SuppressLint({"MissingGetterMatchingBuilder", "ExecutorRegistration"})
+        public Builder setCheckedChangeListener(
+                @NonNull OnCheckedChangeListener onCheckedChangeListener) {
+            this.mOnCheckedChangeListener =
+                    new OnCheckedChangeListenerStub(requireNonNull(onCheckedChangeListener));
+            return this;
+        }
+
+        private Builder(OnCheckedChangeListener onCheckedChangeListener) {
+            this.mOnCheckedChangeListener = new OnCheckedChangeListenerStub(
+                    onCheckedChangeListener);
+        }
+
+        /** Constructs the {@link Toggle} defined by this builder. */
+        @NonNull
+        public Toggle build() {
+            return new Toggle(this);
+        }
+    }
+
+    @Keep // We need to keep these stub for Bundler serialization logic.
+    private static class OnCheckedChangeListenerStub extends IOnCheckedChangeListener.Stub {
+        private final OnCheckedChangeListener mOnCheckedChangeListener;
+
+        private OnCheckedChangeListenerStub(OnCheckedChangeListener onCheckedChangeListener) {
+            this.mOnCheckedChangeListener = onCheckedChangeListener;
+        }
+
+        @Override
+        public void onCheckedChange(boolean isChecked, IOnDoneCallback callback) {
+            RemoteUtils.dispatchHostCall(
+                    () -> mOnCheckedChangeListener.onCheckedChange(isChecked), callback,
+                    "onCheckedChange");
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java
new file mode 100644
index 0000000..89857f8
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java
@@ -0,0 +1,248 @@
+/*
+ * 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.car.app.model.constraints;
+
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.Action.ActionType;
+import androidx.car.app.model.CarText;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Encapsulates the constraints to apply when rendering a list of {@link Action}s on a template.
+ */
+public class ActionsConstraints {
+
+    /** Conservative constraints for most template types. */
+    @NonNull
+    private static final ActionsConstraints ACTIONS_CONSTRAINTS_CONSERVATIVE =
+            ActionsConstraints.builder().setMaxActions(2).build();
+
+    /**
+     * Constraints for template headers, where only the special-purpose back and app-icon standard
+     * actions are allowed.
+     */
+    @NonNull
+    public static final ActionsConstraints ACTIONS_CONSTRAINTS_HEADER =
+            ActionsConstraints.builder().setMaxActions(1).addDisallowedActionType(
+                    Action.TYPE_CUSTOM).build();
+
+    /**
+     * Default constraints that should be applied to most templates (2 actions, 1 can have
+     * title).
+     */
+    @NonNull
+    public static final ActionsConstraints ACTIONS_CONSTRAINTS_SIMPLE =
+            ACTIONS_CONSTRAINTS_CONSERVATIVE.newBuilder().setMaxCustomTitles(1).build();
+
+    /** Constraints for navigation templates. */
+    @NonNull
+    public static final ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION =
+            ACTIONS_CONSTRAINTS_CONSERVATIVE
+                    .newBuilder()
+                    .setMaxActions(4)
+                    .setMaxCustomTitles(1)
+                    .addRequiredActionType(Action.TYPE_CUSTOM)
+                    .build();
+
+    private final int mMaxActions;
+    private final int mMaxCustomTitles;
+    private final Set<Integer> mRequiredActionTypes;
+    private final Set<Integer> mDisallowedActionTypes;
+
+    /** Returns a builder of {@link ActionsConstraints}. */
+    @VisibleForTesting
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns a new builder that contains the same data as this {@link ActionsConstraints}
+     * instance.
+     */
+    @VisibleForTesting
+    @NonNull
+    public Builder newBuilder() {
+        return new Builder(this);
+    }
+
+    /** Returns the max number of actions allowed. */
+    public int getMaxActions() {
+        return mMaxActions;
+    }
+
+    /** Returns the max number of actions with custom titles allowed. */
+    public int getMaxCustomTitles() {
+        return mMaxCustomTitles;
+    }
+
+    /** Adds the set of required action types. */
+    @NonNull
+    public Set<Integer> getRequiredActionTypes() {
+        return mRequiredActionTypes;
+    }
+
+    /** Adds the set of disallowed action types. */
+    @NonNull
+    public Set<Integer> getDisallowedActionTypes() {
+        return mDisallowedActionTypes;
+    }
+
+    /**
+     * Validates the input list of {@link Action}s against this {@link ActionsConstraints} instance.
+     *
+     * @throws IllegalArgumentException if the actions has more actions than allowed.
+     * @throws IllegalArgumentException if the actions has more actions with custom titles than
+     *                                  allowed.
+     * @throws IllegalArgumentException if the actions does not contain all required types.
+     * @throws IllegalArgumentException if the actions contain any disallowed types.
+     */
+    public void validateOrThrow(@NonNull List<Object> actions) {
+        int maxAllowedActions = mMaxActions;
+        int maxAllowedCustomTitles = mMaxCustomTitles;
+
+        Set<Integer> requiredTypes =
+                mRequiredActionTypes.isEmpty()
+                        ? Collections.emptySet()
+                        : new HashSet<>(this.mRequiredActionTypes);
+
+        for (Object object : actions) {
+            if (object instanceof Action) {
+                Action action = (Action) object;
+
+                if (mDisallowedActionTypes.contains(action.getType())) {
+                    throw new IllegalArgumentException(
+                            Action.typeToString(action.getType()) + " is disallowed");
+                }
+
+                requiredTypes.remove(action.getType());
+
+                CarText title = action.getTitle();
+                if (title != null && !title.isEmpty()) {
+                    if (--maxAllowedCustomTitles < 0) {
+                        throw new IllegalArgumentException(
+                                "Action strip exceeded max number of "
+                                        + mMaxCustomTitles
+                                        + " actions with custom titles");
+                    }
+                }
+
+                if (--maxAllowedActions < 0) {
+                    throw new IllegalArgumentException(
+                            "Action strip exceeded max number of " + mMaxActions + " actions");
+                }
+
+            } else {
+                throw new IllegalArgumentException("Unsupported action: " + object);
+            }
+        }
+
+        if (!requiredTypes.isEmpty()) {
+            StringBuilder missingTypeError = new StringBuilder();
+            for (@ActionType int type : requiredTypes) {
+                missingTypeError.append(Action.typeToString(type)).append(",");
+            }
+            throw new IllegalArgumentException(
+                    "Missing required action types: " + missingTypeError);
+        }
+    }
+
+    private ActionsConstraints(Builder builder) {
+        mMaxActions = builder.mMaxActions;
+        mMaxCustomTitles = builder.mMaxCustomTitles;
+        mRequiredActionTypes = new HashSet<>(builder.mRequiredActionTypes);
+
+        if (!builder.mDisallowedActionTypes.isEmpty()) {
+            Set<Integer> disallowedActionTypes = new HashSet<>(builder.mDisallowedActionTypes);
+            disallowedActionTypes.retainAll(mRequiredActionTypes);
+            if (!disallowedActionTypes.isEmpty()) {
+                throw new IllegalArgumentException(
+                        "Disallowed action types cannot also be in the required set.");
+            }
+        }
+        mDisallowedActionTypes = new HashSet<>(builder.mDisallowedActionTypes);
+
+        if (mRequiredActionTypes.size() > mMaxActions) {
+            throw new IllegalArgumentException(
+                    "Required action types exceeded max allowed actions.");
+        }
+    }
+
+    /**
+     * A builder of {@link ActionsConstraints}.
+     */
+    @VisibleForTesting
+    public static final class Builder {
+        private int mMaxActions = Integer.MAX_VALUE;
+        private int mMaxCustomTitles;
+        private final Set<Integer> mRequiredActionTypes = new HashSet<>();
+        private final Set<Integer> mDisallowedActionTypes = new HashSet<>();
+
+        /** Sets the maximum number of actions allowed. */
+        @NonNull
+        public Builder setMaxActions(int maxActions) {
+            this.mMaxActions = maxActions;
+            return this;
+        }
+
+        /** Sets the maximum number of actions with custom titles allowed. */
+        @NonNull
+        public Builder setMaxCustomTitles(int maxCustomTitles) {
+            this.mMaxCustomTitles = maxCustomTitles;
+            return this;
+        }
+
+        /** Adds an action type to the set of required types. */
+        @NonNull
+        public Builder addRequiredActionType(@ActionType int actionType) {
+            mRequiredActionTypes.add(actionType);
+            return this;
+        }
+
+        /** Adds an action type to the set of disallowed types. */
+        @NonNull
+        public Builder addDisallowedActionType(@ActionType int actionType) {
+            mDisallowedActionTypes.add(actionType);
+            return this;
+        }
+
+        /**
+         * Returns an {@link ActionsConstraints} instance defined by this builder.
+         */
+        @NonNull
+        public ActionsConstraints build() {
+            return new ActionsConstraints(this);
+        }
+
+        private Builder() {
+        }
+
+        private Builder(ActionsConstraints constraints) {
+            this.mMaxActions = constraints.mMaxActions;
+            this.mMaxCustomTitles = constraints.mMaxCustomTitles;
+            this.mRequiredActionTypes.addAll(constraints.mRequiredActionTypes);
+            this.mDisallowedActionTypes.addAll(constraints.mDisallowedActionTypes);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/CarColorConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/CarColorConstraints.java
new file mode 100644
index 0000000..489022b
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/CarColorConstraints.java
@@ -0,0 +1,82 @@
+/*
+ * 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.car.app.model.constraints;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarColor.CarColorType;
+
+import java.util.HashSet;
+
+/**
+ * Encapsulates the constraints to apply when rendering a {@link CarColor} on a template.
+ */
+public class CarColorConstraints {
+
+    @NonNull
+    public static final CarColorConstraints UNCONSTRAINED =
+            CarColorConstraints.create(
+                    new int[]{
+                            CarColor.TYPE_CUSTOM,
+                            CarColor.TYPE_DEFAULT,
+                            CarColor.TYPE_PRIMARY,
+                            CarColor.TYPE_SECONDARY,
+                            CarColor.TYPE_RED,
+                            CarColor.TYPE_GREEN,
+                            CarColor.TYPE_BLUE,
+                            CarColor.TYPE_YELLOW
+                    });
+
+    @NonNull
+    public static final CarColorConstraints STANDARD_ONLY =
+            CarColorConstraints.create(
+                    new int[]{
+                            CarColor.TYPE_DEFAULT,
+                            CarColor.TYPE_PRIMARY,
+                            CarColor.TYPE_SECONDARY,
+                            CarColor.TYPE_RED,
+                            CarColor.TYPE_GREEN,
+                            CarColor.TYPE_BLUE,
+                            CarColor.TYPE_YELLOW
+                    });
+
+    @CarColorType
+    private final HashSet<Integer> mAllowedTypes;
+
+    private static CarColorConstraints create(int[] allowedColorTypes) {
+        return new CarColorConstraints(allowedColorTypes);
+    }
+
+    /**
+     * Returns {@code true} if the {@link CarColor} meets the constraints' requirement.
+     *
+     * @throws IllegalArgumentException if the color type is not allowed.
+     */
+    public void validateOrThrow(@NonNull CarColor carColor) {
+        @CarColorType int type = carColor.getType();
+        if (!mAllowedTypes.contains(type)) {
+            throw new IllegalArgumentException("Car color type is not allowed: " + carColor);
+        }
+    }
+
+    private CarColorConstraints(int[] allowedColorTypes) {
+        this.mAllowedTypes = new HashSet<>();
+        for (int type : allowedColorTypes) {
+            this.mAllowedTypes.add(type);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/CarIconConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/CarIconConstraints.java
new file mode 100644
index 0000000..2b9353c
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/CarIconConstraints.java
@@ -0,0 +1,96 @@
+/*
+ * 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.car.app.model.constraints;
+
+import android.content.ContentResolver;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarIcon;
+import androidx.core.graphics.drawable.IconCompat;
+
+/**
+ * Encapsulates the constraints to apply when rendering a {@link CarIcon} on a template.
+ */
+public class CarIconConstraints {
+    /** Allow all custom icon types. */
+    @NonNull
+    public static final CarIconConstraints UNCONSTRAINED =
+            CarIconConstraints.create(
+                    new int[]{
+                            IconCompat.TYPE_BITMAP,
+                            IconCompat.TYPE_RESOURCE,
+                            IconCompat.TYPE_URI
+                    });
+
+    /** By default, do not allow custom icon types that would load asynchronously in the host. */
+    @NonNull
+    public static final CarIconConstraints DEFAULT =
+            CarIconConstraints.create(new int[]{IconCompat.TYPE_BITMAP, IconCompat.TYPE_RESOURCE});
+
+    private final int[] mAllowedTypes;
+
+    private static CarIconConstraints create(int[] allowedCustomIconTypes) {
+        return new CarIconConstraints(allowedCustomIconTypes);
+    }
+
+    /**
+     * Returns {@code true} if the {@link CarIcon} meets the constraints' requirement.
+     *
+     * @throws IllegalStateException    if the custom icon does not have a backing
+     *                                  {@link IconCompat}
+     *                                  instance.
+     * @throws IllegalArgumentException if the custom icon type is not allowed.
+     */
+    public void validateOrThrow(@Nullable CarIcon carIcon) {
+        if (carIcon == null || carIcon.getType() != CarIcon.TYPE_CUSTOM) {
+            return;
+        }
+
+        IconCompat iconCompat = carIcon.getIcon();
+        if (iconCompat == null) {
+            throw new IllegalStateException("Custom icon does not have a backing IconCompat");
+        }
+
+        checkSupportedIcon(iconCompat);
+    }
+
+    /**
+     * Checks whether the given icon is supported.
+     *
+     * @throws IllegalArgumentException if the given icon type is unsupported.
+     */
+    @NonNull
+    public IconCompat checkSupportedIcon(@NonNull IconCompat iconCompat) {
+        int type = iconCompat.getType();
+        for (int allowedType : mAllowedTypes) {
+            if (type == allowedType) {
+                if (type == IconCompat.TYPE_URI
+                        && !ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(
+                        iconCompat.getUri().getScheme())) {
+                    throw new IllegalArgumentException("Unsupported URI scheme for: " + iconCompat);
+                }
+                return iconCompat;
+            }
+        }
+        throw new IllegalArgumentException("Custom icon type is not allowed: " + type);
+    }
+
+    private CarIconConstraints(int[] allowedCustomIconTypes) {
+        this.mAllowedTypes = allowedCustomIconTypes;
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java
new file mode 100644
index 0000000..28b8051
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java
@@ -0,0 +1,264 @@
+/*
+ * 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.car.app.model.constraints;
+
+import static androidx.car.app.model.Row.ROW_FLAG_SHOW_DIVIDERS;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Row.RowFlags;
+
+/**
+ * Encapsulates the constraints to apply when rendering a {@link
+ * androidx.car.app.model.Row} in different contexts.
+ */
+public class RowConstraints {
+    @NonNull
+    public static final RowConstraints UNCONSTRAINED = RowConstraints.builder().build();
+
+    /** Conservative constraints for a row. */
+    @NonNull
+    public static final RowConstraints ROW_CONSTRAINTS_CONSERVATIVE =
+            RowConstraints.builder()
+                    .setMaxActionsExclusive(0)
+                    .setImageAllowed(false)
+                    .setMaxTextLinesPerRow(1)
+                    .setOnClickListenerAllowed(true)
+                    .setToggleAllowed(false)
+                    .build();
+
+    /** The constraints for a full-width row in a pane. */
+    @NonNull
+    public static final RowConstraints ROW_CONSTRAINTS_PANE =
+            RowConstraints.builder()
+                    .setMaxActionsExclusive(2)
+                    .setImageAllowed(true)
+                    .setMaxTextLinesPerRow(2)
+                    .setToggleAllowed(false)
+                    .setOnClickListenerAllowed(false)
+                    .build();
+
+    /** The constraints for a simple row (2 rows of text and 1 image */
+    @NonNull
+    public static final RowConstraints ROW_CONSTRAINTS_SIMPLE =
+            RowConstraints.builder()
+                    .setFlagOverrides(ROW_FLAG_SHOW_DIVIDERS)
+                    .setMaxActionsExclusive(0)
+                    .setImageAllowed(true)
+                    .setMaxTextLinesPerRow(2)
+                    .setToggleAllowed(false)
+                    .setOnClickListenerAllowed(true)
+                    .build();
+
+    /** The constraints for a full-width row in a list (simple + toggle support). */
+    @NonNull
+    public static final RowConstraints ROW_CONSTRAINTS_FULL_LIST =
+            ROW_CONSTRAINTS_SIMPLE.newBuilder().setToggleAllowed(true).build();
+
+    private final int mMaxTextLinesPerRow;
+    private final int mMaxActionsExclusive;
+    private final boolean mIsImageAllowed;
+    private final boolean mIsToggleAllowed;
+    private final boolean mIsOnClickListenerAllowed;
+    @RowFlags
+    private final int mFlagOverrides;
+    private final CarIconConstraints mCarIconConstraints;
+
+    /**
+     * Returns a new {@link Builder}.
+     */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns a new builder that contains the same data as this {@link RowConstraints} instance.
+     */
+    @NonNull
+    public Builder newBuilder() {
+        return new Builder(this);
+    }
+
+    /** Returns whether the row can have a click listener associated with it. */
+    public boolean isOnClickListenerAllowed() {
+        return mIsOnClickListenerAllowed;
+    }
+
+    /** Returns the maximum number lines of text, excluding the title, to render in the row. */
+    public int getMaxTextLinesPerRow() {
+        return mMaxTextLinesPerRow;
+    }
+
+    /** Returns the maximum number actions to allowed in a row that consists only of actions. */
+    public int getMaxActionsExclusive() {
+        return mMaxActionsExclusive;
+    }
+
+    /** Returns whether a toggle can be added to the row. */
+    public boolean isToggleAllowed() {
+        return mIsToggleAllowed;
+    }
+
+    /** Returns whether an image can be added to the row. */
+    public boolean isImageAllowed() {
+        return mIsImageAllowed;
+    }
+
+    /**
+     * The flags that will be forced on each row, on top of whatever flags come from the client
+     * side.
+     */
+    @RowFlags
+    public int getFlagOverrides() {
+        return mFlagOverrides;
+    }
+
+    /** Returns the {@link CarIconConstraints} enforced for the row images. */
+    @NonNull
+    public CarIconConstraints getCarIconConstraints() {
+        return mCarIconConstraints;
+    }
+
+    /**
+     * Validates that the given row satisfies this {@link RowConstraints} instance.
+     *
+     * @throws IllegalArgumentException if the constraints are not met.
+     */
+    public void validateOrThrow(@NonNull Object rowObj) {
+        Row row = (Row) rowObj;
+
+        if (!mIsOnClickListenerAllowed && row.getOnClickListener() != null) {
+            throw new IllegalArgumentException("A click listener is not allowed on the row");
+        }
+
+        if (!mIsToggleAllowed && row.getToggle() != null) {
+            throw new IllegalArgumentException("A toggle is not allowed on the row");
+        }
+
+        CarIcon image = row.getImage();
+        if (image != null) {
+            if (!mIsImageAllowed) {
+                throw new IllegalArgumentException("An image is not allowed on the row");
+            }
+
+            mCarIconConstraints.validateOrThrow(image);
+        }
+
+        if (row.getTexts().size() > mMaxTextLinesPerRow) {
+            throw new IllegalArgumentException(
+                    "The number of lines of texts for the row exceeded the supported max of "
+                            + mMaxTextLinesPerRow);
+        }
+    }
+
+    private RowConstraints(Builder builder) {
+        mIsOnClickListenerAllowed = builder.mIsOnClickListenerAllowed;
+        mMaxTextLinesPerRow = builder.mMaxTextLines;
+        mMaxActionsExclusive = builder.mMaxActionsExclusive;
+        mIsToggleAllowed = builder.mIsToggleAllowed;
+        mIsImageAllowed = builder.mIsImageAllowed;
+        mFlagOverrides = builder.mFlagOverrides;
+        mCarIconConstraints = builder.mCarIconConstraints;
+    }
+
+    /** A builder of {@link RowConstraints}. */
+    public static final class Builder {
+        private boolean mIsOnClickListenerAllowed = true;
+        private boolean mIsToggleAllowed = true;
+        private int mMaxTextLines = Integer.MAX_VALUE;
+        private int mMaxActionsExclusive = Integer.MAX_VALUE;
+        private boolean mIsImageAllowed = true;
+        @RowFlags
+        private int mFlagOverrides;
+        private CarIconConstraints mCarIconConstraints = CarIconConstraints.UNCONSTRAINED;
+
+        /** Sets whether the row can have a click listener associated with it. */
+        @NonNull
+        public Builder setOnClickListenerAllowed(boolean isOnClickListenerAllowed) {
+            this.mIsOnClickListenerAllowed = isOnClickListenerAllowed;
+            return this;
+        }
+
+        /** Sets the maximum number lines of text, excluding the title, to render in the row. */
+        @NonNull
+        public Builder setMaxTextLinesPerRow(int maxTextLinesPerRow) {
+            this.mMaxTextLines = maxTextLinesPerRow;
+            return this;
+        }
+
+        /** Sets the maximum number actions to allowed in a row that consists only of actions. */
+        @NonNull
+        public Builder setMaxActionsExclusive(int maxActionsExclusive) {
+            this.mMaxActionsExclusive = maxActionsExclusive;
+            return this;
+        }
+
+        /** Sets whether an image can be added to the row. */
+        @NonNull
+        public Builder setImageAllowed(boolean imageAllowed) {
+            this.mIsImageAllowed = imageAllowed;
+            return this;
+        }
+
+        /** Sets whether a toggle can be added to the row. */
+        @NonNull
+        public Builder setToggleAllowed(boolean toggleAllowed) {
+            this.mIsToggleAllowed = toggleAllowed;
+            return this;
+        }
+
+        /**
+         * Sets the flags that will be forced on each row, on top of whatever flags come from
+         * the client side.
+         */
+        @NonNull
+        public Builder setFlagOverrides(@RowFlags int flagOverrides) {
+            this.mFlagOverrides = flagOverrides;
+            return this;
+        }
+
+        /** Sets the {@link CarIconConstraints} enforced for the row images. */
+        @NonNull
+        public Builder setCarIconConstraints(@NonNull CarIconConstraints carIconConstraints) {
+            this.mCarIconConstraints = carIconConstraints;
+            return this;
+        }
+
+        /**
+         * Constructs the {@link RowConstraints} defined by this builder.
+         */
+        @NonNull
+        public RowConstraints build() {
+            return new RowConstraints(this);
+        }
+
+        private Builder() {
+        }
+
+        private Builder(RowConstraints constraints) {
+            mIsOnClickListenerAllowed = constraints.mIsOnClickListenerAllowed;
+            mMaxTextLines = constraints.mMaxTextLinesPerRow;
+            mMaxActionsExclusive = constraints.mMaxActionsExclusive;
+            mIsToggleAllowed = constraints.mIsToggleAllowed;
+            mIsImageAllowed = constraints.mIsImageAllowed;
+            mFlagOverrides = constraints.mFlagOverrides;
+            mCarIconConstraints = constraints.mCarIconConstraints;
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java
new file mode 100644
index 0000000..5d20a94
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java
@@ -0,0 +1,271 @@
+/*
+ * 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.car.app.model.constraints;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.model.constraints.RowConstraints.ROW_CONSTRAINTS_CONSERVATIVE;
+import static androidx.car.app.model.constraints.RowConstraints.ROW_CONSTRAINTS_FULL_LIST;
+import static androidx.car.app.model.constraints.RowConstraints.ROW_CONSTRAINTS_PANE;
+import static androidx.car.app.model.constraints.RowConstraints.ROW_CONSTRAINTS_SIMPLE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.ActionList;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.Pane;
+import androidx.car.app.model.SectionedItemList;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Encapsulates the constraints to apply when rendering a row list under different contexts.
+ */
+public class RowListConstraints {
+    /**
+     * The RowList that is used for max row.
+     *
+     * @hide
+     */
+    // TODO(shiufai): investigate how to expose IntDefs if needed.
+    @IntDef(value = {DEFAULT_LIST, PANE, ROUTE_PREVIEW})
+    @RestrictTo(LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ListType {
+    }
+
+    @ListType
+    public static final int DEFAULT_LIST = 0;
+
+    @ListType
+    public static final int PANE = 1;
+
+    @ListType
+    public static final int ROUTE_PREVIEW = 2;
+
+    /** Conservative constraints for all types lists. */
+    @NonNull
+    public static final RowListConstraints ROW_LIST_CONSTRAINTS_CONSERVATIVE =
+            RowListConstraints.builder()
+                    .setRowListType(DEFAULT_LIST)
+                    .setMaxActions(0)
+                    .setRowConstraints(ROW_CONSTRAINTS_CONSERVATIVE)
+                    .setAllowSelectableLists(false)
+                    .build();
+
+    /** Default constraints for heterogeneous pane of items, full width. */
+    @NonNull
+    public static final RowListConstraints ROW_LIST_CONSTRAINTS_PANE =
+            ROW_LIST_CONSTRAINTS_CONSERVATIVE
+                    .newBuilder()
+                    .setMaxActions(2)
+                    .setRowListType(PANE)
+                    .setRowConstraints(ROW_CONSTRAINTS_PANE)
+                    .setAllowSelectableLists(false)
+                    .build();
+
+    /** Default constraints for uniform lists of items, no toggles. */
+    @NonNull
+    public static final RowListConstraints ROW_LIST_CONSTRAINTS_SIMPLE =
+            ROW_LIST_CONSTRAINTS_CONSERVATIVE
+                    .newBuilder()
+                    .setRowConstraints(ROW_CONSTRAINTS_SIMPLE)
+                    .build();
+
+    /** Default constraints for the route preview card. */
+    @NonNull
+    public static final RowListConstraints ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW =
+            ROW_LIST_CONSTRAINTS_CONSERVATIVE
+                    .newBuilder()
+                    .setRowListType(ROUTE_PREVIEW)
+                    .setRowConstraints(ROW_CONSTRAINTS_SIMPLE)
+                    .setAllowSelectableLists(true)
+                    .build();
+
+    /** Default constraints for uniform lists of items, full width (simple + toggle support). */
+    @NonNull
+    public static final RowListConstraints ROW_LIST_CONSTRAINTS_FULL_LIST =
+            ROW_LIST_CONSTRAINTS_CONSERVATIVE
+                    .newBuilder()
+                    .setRowConstraints(ROW_CONSTRAINTS_FULL_LIST)
+                    .setAllowSelectableLists(true)
+                    .build();
+
+    @ListType
+    private final int mRowListType;
+    private final int mMaxActions;
+    private final RowConstraints mRowConstraints;
+    private final boolean mAllowSelectableLists;
+
+    /** A builder of {@link RowListConstraints}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** Return a a new builder for this {@link RowListConstraints} instance. */
+    @NonNull
+    public Builder newBuilder() {
+        return new Builder(this);
+    }
+
+    /** Returns the row list type for this constraint. */
+    @ListType
+    public int getRowListType() {
+        return mRowListType;
+    }
+
+    /** Returns the maximum number of actions allowed to be added alongside the list. */
+    public int getMaxActions() {
+        return mMaxActions;
+    }
+
+    /** Returns the constraints to apply on individual rows. */
+    @NonNull
+    public RowConstraints getRowConstraints() {
+        return mRowConstraints;
+    }
+
+    /** Returns whether selectable lists are allowed. */
+    public boolean isAllowSelectableLists() {
+        return mAllowSelectableLists;
+    }
+
+    /**
+     * Validates that the {@link ItemList} satisfies this {@link RowListConstraints} instance.
+     *
+     * @throws IllegalArgumentException if the constraints are not met.
+     */
+    public void validateOrThrow(@NonNull ItemList itemList) {
+        if (itemList.getOnSelectedListener() != null && !mAllowSelectableLists) {
+            throw new IllegalArgumentException("Selectable lists are not allowed");
+        }
+
+        validateRows(itemList.getItems());
+    }
+
+    /**
+     * Validates that the list of {@link SectionedItemList}s satisfies this
+     * {@link RowListConstraints}
+     * instance.
+     *
+     * @throws IllegalArgumentException if the constraints are not met.
+     */
+    public void validateOrThrow(@NonNull List<SectionedItemList> sections) {
+        List<Object> combinedLists = new ArrayList<>();
+
+        for (SectionedItemList section : sections) {
+            ItemList sectionList = section.getItemList();
+            if (sectionList.getOnSelectedListener() != null && !mAllowSelectableLists) {
+                throw new IllegalArgumentException("Selectable lists are not allowed");
+            }
+
+            combinedLists.addAll(sectionList.getItems());
+        }
+
+        validateRows(combinedLists);
+    }
+
+    /**
+     * Validates that the {@link Pane} satisfies this {@link RowListConstraints} instance.
+     *
+     * @throws IllegalArgumentException if the constraints are not met.
+     */
+    public void validateOrThrow(@NonNull Pane pane) {
+        ActionList actions = pane.getActionList();
+        if (actions != null && actions.getList().size() > mMaxActions) {
+            throw new IllegalArgumentException(
+                    "The number of actions on the pane exceeded the supported max of "
+                            + mMaxActions);
+        }
+
+        validateRows(pane.getRows());
+    }
+
+    private void validateRows(List<Object> rows) {
+        for (Object rowObj : rows) {
+            mRowConstraints.validateOrThrow(rowObj);
+        }
+    }
+
+    private RowListConstraints(Builder builder) {
+        mMaxActions = builder.mMaxActions;
+        mRowConstraints = builder.mRowConstraints;
+        mAllowSelectableLists = builder.mAllowSelectableLists;
+        mRowListType = builder.mRowListType;
+    }
+
+    /**
+     * A builder of {@link RowListConstraints}.
+     */
+    public static final class Builder {
+        @ListType
+        private int mRowListType;
+        private int mMaxActions;
+        private RowConstraints mRowConstraints = RowConstraints.UNCONSTRAINED;
+        private boolean mAllowSelectableLists;
+
+        /** Sets the row list type for this constraint. */
+        @NonNull
+        public Builder setRowListType(@ListType int rowListType) {
+            this.mRowListType = rowListType;
+            return this;
+        }
+
+        /** Sets the maximum number of actions allowed to be added alongside the list. */
+        @NonNull
+        public Builder setMaxActions(int maxActions) {
+            this.mMaxActions = maxActions;
+            return this;
+        }
+
+        /** Sets the constraints to apply on individual rows. */
+        @NonNull
+        public Builder setRowConstraints(@NonNull RowConstraints rowConstraints) {
+            this.mRowConstraints = rowConstraints;
+            return this;
+        }
+
+        /** Sets whether selectable lists are allowed. */
+        @NonNull
+        public Builder setAllowSelectableLists(boolean allowSelectableLists) {
+            this.mAllowSelectableLists = allowSelectableLists;
+            return this;
+        }
+
+        /**
+         * Constructs the {@link RowListConstraints} defined by this builder.
+         */
+        @NonNull
+        public RowListConstraints build() {
+            return new RowListConstraints(this);
+        }
+
+        private Builder() {
+        }
+
+        private Builder(RowListConstraints constraints) {
+            this.mMaxActions = constraints.mMaxActions;
+            this.mRowConstraints = constraints.mRowConstraints;
+            this.mAllowSelectableLists = constraints.mAllowSelectableLists;
+            this.mRowListType = constraints.mRowListType;
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
new file mode 100644
index 0000000..fd22326
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
@@ -0,0 +1,274 @@
+/*
+ * 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.car.app.navigation;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.car.app.utils.ThreadUtils.checkMainThread;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.CarContext;
+import androidx.car.app.HostDispatcher;
+import androidx.car.app.HostException;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.navigation.model.TravelEstimate;
+import androidx.car.app.navigation.model.Trip;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+import androidx.car.app.utils.RemoteUtils;
+
+/**
+ * Manager for communicating navigation related events with the host.
+ *
+ * <p>Navigation apps must use this interface to coordinate with the car system for navigation
+ * specific resources such as vehicle cluster and heads-up displays.
+ *
+ * <p>When a navigation app receives a user action to start navigating, it should call {@link
+ * #navigationStarted()} to indicate it is currently navigating. When the app receives a user action
+ * to end navigation or when the destination is reached, {@link #navigationEnded()} should be
+ * called.
+ *
+ * <p>Navigation apps must also register a {@link NavigationManagerListener} to handle callbacks to
+ * {@link NavigationManagerListener#stopNavigation()} issued by the host.
+ */
+public class NavigationManager {
+    private final INavigationManager.Stub mNavigationmanager;
+    private final HostDispatcher mHostDispatcher;
+
+    // Guarded by main thread access.
+    @Nullable
+    private NavigationManagerListener mListener;
+    private boolean mIsNavigating;
+    private boolean mIsAutoDriveEnabled;
+
+    /**
+     * Sends the destinations, steps, and trip estimates to the host.
+     *
+     * <p>The data <b>may</b> be rendered at different places in the car such as the instrument
+     * cluster screen or the heads-up display.
+     *
+     * <p>This method should only be invoked once the navigation app has called {@link
+     * #navigationStarted()}, or else the updates will be dropped by the host. Once the app has
+     * called {@link #navigationEnded()} or received
+     * {@link NavigationManagerListener#stopNavigation()} it should stop sending updates.
+     *
+     * <p>As the location changes, and in accordance with speed and rounded distance changes, the
+     * {@link TravelEstimate}s in the provided {@link Trip} should be rebuilt and this method called
+     * again. For example, when the next step is greater than 10 kilometers away and the display
+     * unit is kilometers, updates should occur roughly every kilometer.
+     *
+     * <p>Data provided to the cluster display depends on the vehicle capabilities. In some
+     * instances the information may not be shown at all. On some vehicles {@link
+     * androidx.car.app.navigation.model.Maneuver}s of unknown type may be skipped while on other
+     * displays the associated icon may be shown.
+     *
+     * @throws HostException            if the call is invoked by an app that is not declared as
+     *                                  a navigation app in the manifest.
+     * @throws IllegalStateException    if the call occurs when navigation is not started. See
+     *                                  {@link #navigationStarted()} for more info.
+     * @throws IllegalArgumentException if any of the destinations, steps, or trip position is
+     *                                  not well formed.
+     * @throws IllegalStateException    if the current thread is not the main thread.
+     */
+    @MainThread
+    public void updateTrip(@NonNull Trip trip) {
+        checkMainThread();
+        if (!mIsNavigating) {
+            throw new IllegalStateException("Navigation is not started");
+        }
+
+        Bundleable bundle;
+        try {
+            bundle = Bundleable.create(trip);
+        } catch (BundlerException e) {
+            throw new IllegalArgumentException("Serialization failure", e);
+        }
+
+        mHostDispatcher.dispatch(
+                CarContext.NAVIGATION_SERVICE,
+                (INavigationHost service) -> {
+                    service.updateTrip(bundle);
+                    return null;
+                },
+                "updateTrip");
+    }
+
+    /**
+     * Sets a listener to start receiving navigation manager events, or {@code null} to clear the
+     * listener.
+     *
+     * @throws IllegalStateException if {@code null} is passed in while navigation is started. See
+     *                               {@link #navigationStarted()} for more info.
+     * @throws IllegalStateException if the current thread is not the main thread.
+     */
+    // TODO(rampara): Add Executor parameter.
+    @SuppressLint("ExecutorRegistration")
+    @MainThread
+    public void setListener(@Nullable NavigationManagerListener listener) {
+        checkMainThread();
+        if (mIsNavigating && listener == null) {
+            throw new IllegalStateException("Removing listener while navigating");
+        }
+        this.mListener = listener;
+        if (mIsAutoDriveEnabled && listener != null) {
+            listener.onAutoDriveEnabled();
+        }
+    }
+
+    /**
+     * Notifies the host that the app has started active navigation.
+     *
+     * <p>Only one app may be actively navigating in the car at any time and ownership is managed by
+     * the host. The app must call this method to inform the system that it has started
+     * navigation in response to user action.
+     *
+     * <p>This function can only called if {@link #setListener(NavigationManagerListener)} has been
+     * called with a non-{@code null} value. The listener is required so that a signal to stop
+     * navigation from the host can be handled using
+     * {@link NavigationManagerListener#stopNavigation()}.
+     *
+     * <p>This method is idempotent.
+     *
+     * @throws IllegalStateException if no navigation manager listener has been set.
+     * @throws IllegalStateException if the current thread is not the main thread.
+     */
+    @MainThread
+    public void navigationStarted() {
+        checkMainThread();
+        if (mIsNavigating) {
+            return;
+        }
+        if (mListener == null) {
+            throw new IllegalStateException("No listener has been set");
+        }
+        mIsNavigating = true;
+        mHostDispatcher.dispatch(
+                CarContext.NAVIGATION_SERVICE,
+                (INavigationHost service) -> {
+                    service.navigationStarted();
+                    return null;
+                },
+                "navigationStarted");
+    }
+
+    /**
+     * Notifies the host that the app has ended active navigation.
+     *
+     * <p>Only one app may be actively navigating in the car at any time and ownership is managed by
+     * the host. The app must call this method to inform the system that it has ended navigation,
+     * for example, in response to the user cancelling navigation or upon reaching the destination.
+     *
+     * <p>This method is idempotent.
+     *
+     * @throws IllegalStateException if the current thread is not the main thread.
+     */
+    @MainThread
+    public void navigationEnded() {
+        checkMainThread();
+        if (!mIsNavigating) {
+            return;
+        }
+        mIsNavigating = false;
+        mHostDispatcher.dispatch(
+                CarContext.NAVIGATION_SERVICE,
+                (INavigationHost service) -> {
+                    service.navigationEnded();
+                    return null;
+                },
+                "navigationEnded");
+    }
+
+    /**
+     * Creates an instance of {@link NavigationManager}.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY)
+    @NonNull
+    public static NavigationManager create(@NonNull HostDispatcher hostDispatcher) {
+        return new NavigationManager(hostDispatcher);
+    }
+
+    /**
+     * Returns the {@code INavigationManager.Stub} binder object.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY)
+    @NonNull
+    public INavigationManager.Stub getIInterface() {
+        return mNavigationmanager;
+    }
+
+    /**
+     * Tells the app to stop navigating.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY)
+    @MainThread
+    public void stopNavigation() {
+        checkMainThread();
+        if (!mIsNavigating) {
+            return;
+        }
+        mIsNavigating = false;
+        requireNonNull(mListener).stopNavigation();
+    }
+
+    /**
+     * Signifies that from this point, until {@link
+     * androidx.car.app.CarAppService#onCarAppFinished} is called, any navigation
+     * should automatically start driving to the destination as if the user was moving.
+     *
+     * <p>This is used in a testing environment, allowing testing the navigation app's navigation
+     * capabilities without being in a car.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY)
+    @MainThread
+    public void onAutoDriveEnabled() {
+        checkMainThread();
+        mIsAutoDriveEnabled = true;
+        if (mListener != null) {
+            mListener.onAutoDriveEnabled();
+        }
+    }
+
+    /** @hide */
+    @RestrictTo(LIBRARY_GROUP) // Restrict to testing library
+    @SuppressWarnings({"methodref.receiver.bound.invalid"})
+    protected NavigationManager(@NonNull HostDispatcher hostDispatcher) {
+        this.mHostDispatcher = requireNonNull(hostDispatcher);
+        mNavigationmanager =
+                new INavigationManager.Stub() {
+                    @Override
+                    public void stopNavigation(IOnDoneCallback callback) {
+                        RemoteUtils.dispatchHostCall(
+                                NavigationManager.this::stopNavigation, callback, "stopNavigation");
+                    }
+                };
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java
new file mode 100644
index 0000000..ee4c29f
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java
@@ -0,0 +1,54 @@
+/*
+ * 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.car.app.navigation;
+
+import android.annotation.SuppressLint;
+
+import androidx.car.app.navigation.model.Trip;
+
+/**
+ * Listener of events from the {@link NavigationManager}.
+ *
+ * @see NavigationManager
+ */
+public interface NavigationManagerListener {
+    /**
+     * Notifies the app to stop active navigation, which may occurs when another source such as the
+     * car head unit starts navigating.
+     *
+     * <p>When receiving this callback, the app must stop all routing including navigation voice
+     * guidance, routing-related notifications, and updating trip information via {@link
+     * NavigationManager#updateTrip(Trip)}.
+     */
+
+    // TODO(rampara): Listener method names must follow the on<Something> style. Consider
+    //  onShouldStopNavigation.
+    @SuppressLint("CallbackMethodName")
+    void stopNavigation();
+
+    /**
+     * Notifies the app that, from this point onwards, when the user chooses to navigate to a
+     * destination, the app should start simulating a drive towards that destination.
+     *
+     * <p>This mode should remain active until {@link
+     * androidx.car.app.CarAppService#onCarAppFinished} is called.
+     *
+     * <p>This functionality is used to allow verifying the app's navigation capabilities without
+     * being in an actual car.
+     */
+    void onAutoDriveEnabled();
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Destination.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Destination.java
new file mode 100644
index 0000000..efcd9e6
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Destination.java
@@ -0,0 +1,189 @@
+/*
+ * 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.car.app.navigation.model;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.constraints.CarIconConstraints;
+
+import java.util.Objects;
+
+/** A class representing information related to a destination. */
+public final class Destination {
+    @Keep
+    @Nullable
+    private final CarText mName;
+    @Keep
+    @Nullable
+    private final CarText mAddress;
+    @Keep
+    @Nullable
+    private final CarIcon mImage;
+
+    /**
+     * Constructs a new builder of {@link Destination} with the given name and address.
+     *
+     * @throws NullPointerException if {@code name} is {@code null}.
+     * @throws NullPointerException if {@code address} is {@code null}.
+     */
+    @NonNull
+    public static Builder builder(@NonNull CharSequence name, @NonNull CharSequence address) {
+        return builder().setName(name).setAddress(address);
+    }
+
+    /** Constructs a new builder of {@link Destination}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @Nullable
+    public CarText getName() {
+        return mName;
+    }
+
+    @Nullable
+    public CarText getAddress() {
+        return mAddress;
+    }
+
+    @Nullable
+    public CarIcon getImage() {
+        return mImage;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[name: "
+                + CarText.toShortString(mName)
+                + ", address: "
+                + CarText.toShortString(mAddress)
+                + ", image: "
+                + mImage
+                + "]";
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (!(other instanceof Destination)) {
+            return false;
+        }
+
+        Destination otherDestination = (Destination) other;
+        return Objects.equals(mName, otherDestination.mName)
+                && Objects.equals(mAddress, otherDestination.mAddress)
+                && Objects.equals(mImage, otherDestination.mImage);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mName, mAddress, mImage);
+    }
+
+    private Destination(Builder builder) {
+        this.mName = builder.mName;
+        this.mAddress = builder.mAddress;
+        this.mImage = builder.mImage;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private Destination() {
+        mName = null;
+        mAddress = null;
+        mImage = null;
+    }
+
+    /** A builder of {@link Destination}. */
+    public static final class Builder {
+        @Nullable
+        private CarText mName;
+        @Nullable
+        private CarText mAddress;
+        @Nullable
+        private CarIcon mImage;
+
+        /**
+         * Sets the destination name formatted for the user's current locale, or {@code null} to not
+         * display a destination name.
+         */
+        @NonNull
+        public Builder setName(@Nullable CharSequence name) {
+            this.mName = name == null ? null : CarText.create(name);
+            return this;
+        }
+
+        /**
+         * Sets the destination address formatted for the user's current locale, or {@code null}
+         * to not
+         * display an address.
+         */
+        @NonNull
+        public Builder setAddress(@Nullable CharSequence address) {
+            this.mAddress = address == null ? null : CarText.create(address);
+            return this;
+        }
+
+        /**
+         * Sets the destination image to display, or {@code null} to not display an image.
+         *
+         * <h4>Image Sizing Guidance</h4>
+         *
+         * The provided image should have a maximum size of 64 x 64 dp. If the image exceeds this
+         * maximum size in either one of the dimensions, it will be scaled down and centered
+         * inside the
+         * bounding box while preserving the aspect ratio.
+         *
+         * <p>See {@link CarIcon} for more details related to providing icon and image resources
+         * that
+         * work with different car screen pixel densities.
+         */
+        @NonNull
+        public Builder setImage(@Nullable CarIcon image) {
+            CarIconConstraints.DEFAULT.validateOrThrow(image);
+            this.mImage = image;
+            return this;
+        }
+
+        /**
+         * Constructs the {@link Destination} defined by this builder.
+         *
+         * <p>At least one of the name or the address must be set and not empty.
+         *
+         * @throws IllegalStateException if both the name and the address are {@code null} or empty.
+         * @see #setName(CharSequence)
+         * @see #setAddress(CharSequence)
+         */
+        @NonNull
+        public Destination build() {
+            if ((mName == null || mName.isEmpty()) && (mAddress == null || mAddress.isEmpty())) {
+                throw new IllegalStateException("Both name and address cannot be null or empty");
+            }
+            return new Destination(this);
+        }
+
+        private Builder() {
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Lane.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Lane.java
new file mode 100644
index 0000000..335035d
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Lane.java
@@ -0,0 +1,115 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Configuration of a single lane of a road at a particular point in the navigation.
+ *
+ * <p>A {@link Lane} object describes all possible directions the driver could go from this lane,
+ * and indicates which directions the driver could take to stay on the navigation route.
+ */
+public final class Lane {
+    @Keep
+    private final List<LaneDirection> mDirections;
+
+    /** Constructs a new builder of {@link Lane}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @NonNull
+    public List<LaneDirection> getDirections() {
+        return mDirections;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[direction count: " + (mDirections != null ? mDirections.size() : 0) + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mDirections);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof Lane)) {
+            return false;
+        }
+
+        Lane otherLane = (Lane) other;
+        return Objects.equals(mDirections, otherLane.mDirections);
+    }
+
+    private Lane(List<LaneDirection> directions) {
+        this.mDirections = new ArrayList<>(directions);
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private Lane() {
+        mDirections = Collections.emptyList();
+    }
+
+    /** A builder of {@link Lane}. */
+    public static final class Builder {
+        private final List<LaneDirection> mDirections = new ArrayList<>();
+
+        /**
+         * Adds a direction a driver can take from this lane.
+         *
+         * @throws NullPointerException if {@code direction} is {@code null}.
+         */
+        @NonNull
+        public Builder addDirection(@NonNull LaneDirection direction) {
+            mDirections.add(requireNonNull(direction));
+            return this;
+        }
+
+        /**
+         * Clears any directions that may have been added with
+         * {@link #addDirection(LaneDirection)} up to this point.
+         */
+        @NonNull
+        public Builder clearDirections() {
+            mDirections.clear();
+            return this;
+        }
+
+        /** Constructs the {@link Lane} defined by this builder. */
+        @NonNull
+        public Lane build() {
+            return new Lane(mDirections);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/LaneDirection.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/LaneDirection.java
new file mode 100644
index 0000000..e06a6d5
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/LaneDirection.java
@@ -0,0 +1,173 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Defines the possible directions a driver can go when using a particular lane at a particular step
+ * in the navigation.
+ *
+ * <p>These directions can be combined and sent to the host to display a lane configuration to the
+ * user.
+ */
+public final class LaneDirection {
+    /**
+     * Turn amount and direction.
+     *
+     * @hide
+     */
+    @IntDef({
+            SHAPE_UNKNOWN,
+            SHAPE_STRAIGHT,
+            SHAPE_SLIGHT_LEFT,
+            SHAPE_SLIGHT_RIGHT,
+            SHAPE_NORMAL_LEFT,
+            SHAPE_NORMAL_RIGHT,
+            SHAPE_SHARP_LEFT,
+            SHAPE_SHARP_RIGHT,
+            SHAPE_U_TURN_LEFT,
+            SHAPE_U_TURN_RIGHT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY)
+    public @interface Shape {
+    }
+
+    /** The shape is unknown, in which case no lane information should be shown. */
+    @Shape
+    public static final int SHAPE_UNKNOWN = 1;
+
+    /** No turn. */
+    @Shape
+    public static final int SHAPE_STRAIGHT = 2;
+
+    /** Slight left turn, from 10 (included) to 45 (excluded) degrees. */
+    @Shape
+    public static final int SHAPE_SLIGHT_LEFT = 3;
+
+    /** Slight right turn, from 10 (included) to 45 (excluded) degrees. */
+    @Shape
+    public static final int SHAPE_SLIGHT_RIGHT = 4;
+
+    /** Regular left turn, from 45 (included) to 135 (excluded) degrees. */
+    @Shape
+    public static final int SHAPE_NORMAL_LEFT = 5;
+
+    /** Regular right turn, from 45 (included) to 135 (excluded) degrees. */
+    @Shape
+    public static final int SHAPE_NORMAL_RIGHT = 6;
+
+    /** Sharp left turn, from 135 (included) to 175 (excluded) degrees. */
+    @Shape
+    public static final int SHAPE_SHARP_LEFT = 7;
+
+    /** Sharp right turn, from 135 (included) to 175 (excluded) degrees. */
+    @Shape
+    public static final int SHAPE_SHARP_RIGHT = 8;
+
+    /**
+     * A left turn onto the opposite side of the same street, from 175 (included) to 180 (included)
+     * degrees
+     */
+    @Shape
+    public static final int SHAPE_U_TURN_LEFT = 9;
+
+    /**
+     * A right turn onto the opposite side of the same street, from 175 (included) to 180 (included)
+     * degrees
+     */
+    @Shape
+    public static final int SHAPE_U_TURN_RIGHT = 10;
+
+    @Keep
+    @Shape
+    private final int mShape;
+    @Keep
+    private final boolean mIsHighlighted;
+
+    /**
+     * Constructs a new instance of a {@link LaneDirection}.
+     *
+     * @param shape         one of the {@code SHAPE_*} static constants defined in this class.
+     * @param isHighlighted indicates whether the {@link LaneDirection} is the one the driver should
+     *                      take in order to stay on the navigation route.
+     */
+    @NonNull
+    public static LaneDirection create(@Shape int shape, boolean isHighlighted) {
+        return new LaneDirection(shape, isHighlighted);
+    }
+
+    /** Returns shape of this lane direction. */
+    @Shape
+    public int getShape() {
+        return mShape;
+    }
+
+    /**
+     * Returns whether this is a direction the driver should take in order to stay on the navigation
+     * route.
+     */
+    public boolean isHighlighted() {
+        return mIsHighlighted;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[shape: " + mShape + ", isHighlighted: " + mIsHighlighted + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mShape, mIsHighlighted);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof LaneDirection)) {
+            return false;
+        }
+
+        LaneDirection otherDirection = (LaneDirection) other;
+        return mShape == otherDirection.mShape && mIsHighlighted == otherDirection.mIsHighlighted;
+    }
+
+    private LaneDirection(@Shape int shape, boolean isHighlighted) {
+        this.mShape = shape;
+        this.mIsHighlighted = isHighlighted;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private LaneDirection() {
+        mShape = SHAPE_UNKNOWN;
+        mIsHighlighted = false;
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java
new file mode 100644
index 0000000..8f1e1f0
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java
@@ -0,0 +1,645 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.constraints.CarIconConstraints;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/** Information about a maneuver that the driver will be required to perform. */
+// TODO: Update when Embedded updates or a scheme for auto sync is established.
+public final class Maneuver {
+    /**
+     * Possible maneuver types.
+     *
+     * @hide
+     */
+    @IntDef({
+            TYPE_UNKNOWN,
+            TYPE_DEPART,
+            TYPE_NAME_CHANGE,
+            TYPE_KEEP_LEFT,
+            TYPE_KEEP_RIGHT,
+            TYPE_TURN_SLIGHT_LEFT,
+            TYPE_TURN_SLIGHT_RIGHT,
+            TYPE_TURN_NORMAL_LEFT,
+            TYPE_TURN_NORMAL_RIGHT,
+            TYPE_TURN_SHARP_LEFT,
+            TYPE_TURN_SHARP_RIGHT,
+            TYPE_U_TURN_LEFT,
+            TYPE_U_TURN_RIGHT,
+            TYPE_ON_RAMP_SLIGHT_LEFT,
+            TYPE_ON_RAMP_SLIGHT_RIGHT,
+            TYPE_ON_RAMP_NORMAL_LEFT,
+            TYPE_ON_RAMP_NORMAL_RIGHT,
+            TYPE_ON_RAMP_SHARP_LEFT,
+            TYPE_ON_RAMP_SHARP_RIGHT,
+            TYPE_ON_RAMP_U_TURN_LEFT,
+            TYPE_ON_RAMP_U_TURN_RIGHT,
+            TYPE_OFF_RAMP_SLIGHT_LEFT,
+            TYPE_OFF_RAMP_SLIGHT_RIGHT,
+            TYPE_OFF_RAMP_NORMAL_LEFT,
+            TYPE_OFF_RAMP_NORMAL_RIGHT,
+            TYPE_FORK_LEFT,
+            TYPE_FORK_RIGHT,
+            TYPE_MERGE_LEFT,
+            TYPE_MERGE_RIGHT,
+            TYPE_MERGE_SIDE_UNSPECIFIED,
+            TYPE_ROUNDABOUT_ENTER,
+            TYPE_ROUNDABOUT_EXIT,
+            TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW,
+            TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE,
+            TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW,
+            TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE,
+            TYPE_STRAIGHT,
+            TYPE_FERRY_BOAT,
+            TYPE_FERRY_TRAIN,
+            TYPE_DESTINATION,
+            TYPE_DESTINATION_STRAIGHT,
+            TYPE_DESTINATION_LEFT,
+            TYPE_DESTINATION_RIGHT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY)
+    public @interface Type {
+    }
+
+    // LINT.IfChange(enums)
+    /**
+     * Maneuver type is unknown, no maneuver information should be displayed.
+     *
+     * <p>{@link #TYPE_UNKNOWN} may be interpreted differently depending on the consumer. In some
+     * cases the previous maneuver will continue to be shown while in others no maneuver will be
+     * shown at all.
+     */
+    @Type
+    public static final int TYPE_UNKNOWN = 0;
+
+    /**
+     * Starting point of the navigation.
+     *
+     * <p>For example, "Start driving on Main St."
+     */
+    @Type
+    public static final int TYPE_DEPART = 1;
+
+    /**
+     * No turn, but the street name changes.
+     *
+     * <p>For example, "Continue on Main St."
+     */
+    @Type
+    public static final int TYPE_NAME_CHANGE = 2;
+
+    /**
+     * No turn, from 0 (included) to 10 (excluded) degrees.
+     *
+     * <p>This is used in contrast to {@link #TYPE_STRAIGHT} for disambiguating cases where there is
+     * more than one option to go into the same general direction.
+     */
+    @Type
+    public static final int TYPE_KEEP_LEFT = 3;
+
+    /**
+     * No turn, from 0 (included) to 10 (excluded) degrees.
+     *
+     * <p>This is used in contrast to {@link #TYPE_STRAIGHT} for disambiguating cases where there is
+     * more than one option to go into the same general direction.
+     */
+    @Type
+    public static final int TYPE_KEEP_RIGHT = 4;
+
+    /** Slight left turn at an intersection, from 10 (included) to 45 (excluded) degrees. */
+    @Type
+    public static final int TYPE_TURN_SLIGHT_LEFT = 5;
+
+    /** Slight right turn at an intersection, from 10 (included) to 45 (excluded) degrees. */
+    @Type
+    public static final int TYPE_TURN_SLIGHT_RIGHT = 6;
+
+    /** Regular left turn at an intersection, from 45 (included) to 135 (excluded) degrees. */
+    @Type
+    public static final int TYPE_TURN_NORMAL_LEFT = 7;
+
+    /** Regular right turn at an intersection, from 45 (included) to 135 (excluded) degrees. */
+    @Type
+    public static final int TYPE_TURN_NORMAL_RIGHT = 8;
+
+    /** Sharp left turn at an intersection, from 135 (included) to 175 (excluded) degrees. */
+    @Type
+    public static final int TYPE_TURN_SHARP_LEFT = 9;
+
+    /** Sharp right turn at an intersection, from 135 (included) to 175 (excluded) degrees. */
+    @Type
+    public static final int TYPE_TURN_SHARP_RIGHT = 10;
+
+    /**
+     * Left turn onto the opposite side of the same street, from 175 (included) to 180 (included)
+     * degrees.
+     */
+    @Type
+    public static final int TYPE_U_TURN_LEFT = 11;
+
+    /**
+     * A right turn onto the opposite side of the same street, from 175 (included) to 180 (included)
+     * degrees.
+     */
+    @Type
+    public static final int TYPE_U_TURN_RIGHT = 12;
+
+    /**
+     * Slight left turn to enter a turnpike or freeway, from 10 (included) to 45 (excluded) degrees.
+     */
+    @Type
+    public static final int TYPE_ON_RAMP_SLIGHT_LEFT = 13;
+
+    /**
+     * Slight right turn to enter a turnpike or freeway, from 10 (included) to 45 (excluded)
+     * degrees.
+     */
+    @Type
+    public static final int TYPE_ON_RAMP_SLIGHT_RIGHT = 14;
+
+    /**
+     * Regular left turn to enter a turnpike or freeway, from 45 (included) to 135 (excluded)
+     * degrees.
+     */
+    @Type
+    public static final int TYPE_ON_RAMP_NORMAL_LEFT = 15;
+
+    /**
+     * Regular right turn to enter a turnpike or freeway, from 45 (included) to 135 (excluded)
+     * degrees.
+     */
+    @Type
+    public static final int TYPE_ON_RAMP_NORMAL_RIGHT = 16;
+
+    /**
+     * Sharp left turn to enter a turnpike or freeway, from 135 (included) to 175 (excluded)
+     * degrees.
+     */
+    @Type
+    public static final int TYPE_ON_RAMP_SHARP_LEFT = 17;
+
+    /**
+     * Sharp right turn to enter a turnpike or freeway, from 135 (included) to 175 (excluded)
+     * degrees.
+     */
+    @Type
+    public static final int TYPE_ON_RAMP_SHARP_RIGHT = 18;
+
+    /**
+     * Left turn onto the opposite side of the same street to enter a turnpike or freeway, from 175
+     * (included) to 180 (included).
+     */
+    @Type
+    public static final int TYPE_ON_RAMP_U_TURN_LEFT = 19;
+
+    /**
+     * Right turn onto the opposite side of the same street to enter a turnpike or freeway, from 175
+     * (included) to 180 (included).
+     */
+    @Type
+    public static final int TYPE_ON_RAMP_U_TURN_RIGHT = 20;
+
+    /** A left turn to exit a turnpike or freeway, from 10 (included) to 45 (excluded) degrees. */
+    @Type
+    public static final int TYPE_OFF_RAMP_SLIGHT_LEFT = 21;
+
+    /** A right turn to exit a turnpike or freeway, from 10 (included) to 45 (excluded) degrees. */
+    @Type
+    public static final int TYPE_OFF_RAMP_SLIGHT_RIGHT = 22;
+
+    /** A left turn to exit a turnpike or freeway, from 45 (included) to 135 (excluded) degrees. */
+    @Type
+    public static final int TYPE_OFF_RAMP_NORMAL_LEFT = 23;
+
+    /** A left right to exit a turnpike or freeway, from 45 (included) to 135 (excluded) degrees. */
+    @Type
+    public static final int TYPE_OFF_RAMP_NORMAL_RIGHT = 24;
+
+    /**
+     * Keep to the left as the road diverges.
+     *
+     * <p>For example, this is used to indicate "Keep left at the fork".
+     */
+    @Type
+    public static final int TYPE_FORK_LEFT = 25;
+
+    /**
+     * Keep to the right as the road diverges.
+     *
+     * <p>For example, this is used to indicate "Keep right at the fork".
+     */
+    @Type
+    public static final int TYPE_FORK_RIGHT = 26;
+
+    /**
+     * Current road joins another on the left.
+     *
+     * <p>For example, this is used to indicate "Merge left onto Main St.".
+     */
+    @Type
+    public static final int TYPE_MERGE_LEFT = 27;
+
+    /**
+     * Current road joins another on the right.
+     *
+     * <p>For example, this is used to indicate "Merge left onto Main St.".
+     */
+    @Type
+    public static final int TYPE_MERGE_RIGHT = 28;
+
+    /**
+     * Current road joins another without direction specified.
+     *
+     * <p>For example, this is used to indicate "Merge onto Main St.".
+     */
+    @Type
+    public static final int TYPE_MERGE_SIDE_UNSPECIFIED = 29;
+
+    /**
+     * Roundabout entrance on which the current road ends.
+     *
+     * <p>For example, this is used to indicate "Enter the roundabout".
+     */
+    @Type
+    public static final int TYPE_ROUNDABOUT_ENTER = 30;
+
+    /**
+     * Used when leaving a roundabout when the step starts in it.
+     *
+     * <p>For example, this is used to indicate "Exit the roundabout".
+     */
+    @Type
+    public static final int TYPE_ROUNDABOUT_EXIT = 31;
+
+    /**
+     * Enter a clockwise roundabout and take the Nth exit.
+     *
+     * <p>The exit number must be passed when created the maneuver.
+     *
+     * <p>For example, this is used to indicate "At the roundabout, take the Nth exit".
+     */
+    @Type
+    public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW = 32;
+
+    /**
+     * Enter a clockwise roundabout and take the Nth exit after angle A degrees.
+     *
+     * <p>The exit number and angle must be passed when creating the maneuver.
+     *
+     * <p>For example, this is used to indicate "At the roundabout, take the Nth exit".
+     */
+    @Type
+    public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE = 33;
+
+    /**
+     * Enter a counter-clockwise roundabout and take the Nth exit.
+     *
+     * <p>The exit number must be passed when created the maneuver.
+     *
+     * <p>For example, this is used to indicate "At the roundabout, take the Nth exit".
+     */
+    @Type
+    public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW = 34;
+
+    /**
+     * Enter a counter-clockwise roundabout and take the Nth exit after angle A degrees.
+     *
+     * <p>The exit number and angle must be passed when creating the maneuver.
+     *
+     * <p>For example, this is used to indicate "At the roundabout, take a sharp right at the Nth
+     * exit".
+     */
+    @Type
+    public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE = 35;
+
+    /** Driver should steer straight. */
+    @Type
+    public static final int TYPE_STRAIGHT = 36;
+
+    /**
+     * Drive towards a boat ferry for vehicles.
+     *
+     * <p>For example, this is used to indicate "Take the ferry".
+     */
+    @Type
+    public static final int TYPE_FERRY_BOAT = 37;
+
+    /** Drive towards a train ferry for vehicles (e.g. "Take the train"). */
+    @Type
+    public static final int TYPE_FERRY_TRAIN = 38;
+
+    /** Arrival at a destination. */
+    @Type
+    public static final int TYPE_DESTINATION = 39;
+
+    /** Arrival to a destination located straight ahead. */
+    @Type
+    public static final int TYPE_DESTINATION_STRAIGHT = 40;
+
+    /** Arrival to a destination located to the left side of the road. */
+    @Type
+    public static final int TYPE_DESTINATION_LEFT = 41;
+
+    /** Arrival to a destination located to the right side of the road. */
+    @Type
+    public static final int TYPE_DESTINATION_RIGHT = 42;
+    // LINT.ThenChange(:enumTypeChecks)
+
+    @Keep
+    @Type
+    private final int mType;
+    @Keep
+    private final int mRoundaboutExitNumber;
+    @Keep
+    private final int mRoundaboutExitAngle;
+    @Keep
+    @Nullable
+    private final CarIcon mIcon;
+
+    /**
+     * Constructs a new builder of {@link Maneuver}.
+     *
+     * <p>The type should be chosen to reflect the closest semantic meaning of the maneuver. In some
+     * cases, an exact type match is not possible, but choosing a similar or slightly more general
+     * type is preferred. Using {@link #TYPE_UNKNOWN} is allowed, but some headunits will not
+     * display any information in that case.
+     *
+     * @param type one of the {@code TYPE_*} static constants defined in this class.
+     * @throws IllegalArgumentException if {@code type} is not a valid maneuver type.
+     */
+    @NonNull
+    public static Builder builder(@Type int type) {
+        if (!isValidType(type)) {
+            throw new IllegalArgumentException("Maneuver must have a valid type");
+        }
+        return new Builder(type);
+    }
+
+    /**
+     * Returns the maneuver type.
+     *
+     * <p>Required to be set at all times.
+     */
+    @Type
+    public int getType() {
+        return mType;
+    }
+
+    /**
+     * Returns the roundabout exit number, starting from 1 to designate the first exit after joining
+     * the roundabout, and increasing in circulation order. Only relevant if the type is any
+     * variation of {@code TYPE_ROUNDABOUT_ENTER_AND_EXIT_*}.
+     *
+     * <p>For example, if the driver is joining a counter-clockwise roundabout with 4 exits, then
+     * the exit to the right would be exit #1, the one straight ahead would be exit #2, the one
+     * to the left would be exit #3 and the one used by the driver to join the roundabout would
+     * be exit #4.
+     *
+     * <p>Required when the type is a roundabout.
+     */
+    public int getRoundaboutExitNumber() {
+        return mRoundaboutExitNumber;
+    }
+
+    /**
+     * Returns the roundabout exit angle in degrees to designate the amount of distance to travel
+     * around the roundabout. Only relevant if the type is {@link
+     * #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE} or {@link
+     * #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE}.
+     *
+     * <p>For example, if the drive is joining a counter-clockwise roundabout with equally spaced
+     * exits then the exit to the right would be at 45 degrees, the one straight ahead would be
+     * at 90 degrees, the one to the left would at 270 degrees and the one used by the driver to
+     * join the roundabout would be at 360 degrees.
+     *
+     * <p>The angle can also be set for irregular roundabouts. For example a roundabout with three
+     * exits at 90, 270 and 360 degrees could also have the desired exit angle specified.
+     *
+     * <p>Required with the type is a roundabout with an angle.
+     */
+    public int getRoundaboutExitAngle() {
+        return mRoundaboutExitAngle;
+    }
+
+    /**
+     * Returns the icon for the maneuver.
+     *
+     * <p>Optional field that when not set may be shown in the target display by a generic image
+     * representing the specific maneuver.
+     */
+    @Nullable
+    public CarIcon getIcon() {
+        return mIcon;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[type: "
+                + mType
+                + ", exit #: "
+                + mRoundaboutExitNumber
+                + ", exit angle: "
+                + mRoundaboutExitAngle
+                + ", icon: "
+                + mIcon
+                + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mType, mRoundaboutExitNumber, mRoundaboutExitAngle, mIcon);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof Maneuver)) {
+            return false;
+        }
+
+        Maneuver otherManeuver = (Maneuver) other;
+        return mType == otherManeuver.mType
+                && mRoundaboutExitNumber == otherManeuver.mRoundaboutExitNumber
+                && mRoundaboutExitAngle == otherManeuver.mRoundaboutExitAngle
+                && Objects.equals(mIcon, otherManeuver.mIcon);
+    }
+
+    private Maneuver(
+            @Type int type, int roundaboutExitNumber, int roundaboutExitAngle,
+            @Nullable CarIcon icon) {
+        this.mType = type;
+        this.mRoundaboutExitNumber = roundaboutExitNumber;
+        this.mRoundaboutExitAngle = roundaboutExitAngle;
+        CarIconConstraints.DEFAULT.validateOrThrow(icon);
+        this.mIcon = icon;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private Maneuver() {
+        mType = TYPE_UNKNOWN;
+        mRoundaboutExitNumber = 0;
+        mRoundaboutExitAngle = 0;
+        mIcon = null;
+    }
+
+    private static boolean isValidType(@Type int type) {
+        return (type >= TYPE_UNKNOWN && type <= TYPE_DESTINATION_RIGHT);
+    }
+
+    private static boolean isValidTypeWithExitNumber(@Type int type) {
+        return (type == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
+                || type == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
+                || type == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE
+                || type == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE);
+    }
+
+    private static boolean isValidTypeWithExitAngle(@Type int type) {
+        return (type == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE
+                || type == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE);
+    }
+
+    /** A builder of {@link Maneuver}. */
+    public static final class Builder {
+        @Type
+        private final int mType;
+        private boolean mIsRoundaboutExitNumberSet;
+        private int mRoundaboutExitNumber;
+        private boolean mIsRoundaboutExitAngleSet;
+        private int mRoundaboutExitAngle;
+        @Nullable
+        private CarIcon mIcon;
+
+        private Builder(@Type int type) {
+            this.mType = type;
+        }
+
+        /**
+         * Sets an image representing the maneuver, or {@code null} to not set an image for the
+         * maneuver.
+         *
+         * <h4>Image Sizing Guidance</h4>
+         *
+         * The provided image should have a maximum size of 64 x 64 dp. If the image exceeds this
+         * maximum size in either one of the dimensions, it will be scaled down and centered
+         * inside the bounding box while preserving the aspect ratio.
+         *
+         * <p>See {@link CarIcon} for more details related to providing icon and image resources
+         * that work with different car screen pixel densities.
+         */
+        @NonNull
+        public Builder setIcon(@Nullable CarIcon icon) {
+            this.mIcon = icon;
+            return this;
+        }
+
+        /**
+         * Sets an exit number for roundabout maneuvers.
+         *
+         * <p>Use for when {@code type} is {@link #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW}, {@link
+         * #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW},
+         * {@link #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE} or
+         * {@link #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE}. The {@code
+         * roundaboutExitNumber} starts from 1 to designate the first exit after joining the
+         * roundabout, and increases in circulation order.
+         *
+         * <p>For example, if the driver is joining a counter-clockwise roundabout with 4 exits,
+         * then the exit to the right would be exit #1, the one straight ahead would be exit #2,
+         * the one to the left would be exit #3 and the one used by the driver to join the
+         * roundabout would be exit #4.
+         *
+         * @throws IllegalArgumentException if {@code type} does not include a exit number.
+         * @throws IllegalArgumentException if {@code roundaboutExitNumber} is not greater than
+         *                                  zero.
+         */
+        @NonNull
+        public Builder setRoundaboutExitNumber(int roundaboutExitNumber) {
+            if (!isValidTypeWithExitNumber(mType)) {
+                throw new IllegalArgumentException(
+                        "Maneuver does not include roundaboutExitNumber");
+            }
+            if (roundaboutExitNumber < 1) {
+                throw new IllegalArgumentException("Maneuver must include a valid exit number");
+            }
+            this.mIsRoundaboutExitNumberSet = true;
+            this.mRoundaboutExitNumber = roundaboutExitNumber;
+            return this;
+        }
+
+        /**
+         * Sets an exit angle for roundabout maneuvers.
+         *
+         * <p>Use for when {@code type} is {@link #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE} or
+         * {@link #TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE}. The {@code roundaboutExitAngle}
+         * represents the degrees traveled in circulation from the entrance to the exit.
+         *
+         * <p>For example, in a 4 exit example, if all the exits are equally spaced then exit 1
+         * would be at 90 degrees, exit 2 at 180, exit 3 at 270 and exit 4 at 360. However if the
+         * exits are irregular then a different angle could be provided.
+         *
+         * @throws IllegalArgumentException if {@code type} does not include a exit angle.
+         * @throws IllegalArgumentException if {@code roundaboutExitAngle} is not greater than
+         *                                  zero and less than or equal to 360 degrees.
+         */
+        @NonNull
+        public Builder setRoundaboutExitAngle(int roundaboutExitAngle) {
+            if (!isValidTypeWithExitAngle(mType)) {
+                throw new IllegalArgumentException("Maneuver does not include roundaboutExitAngle");
+            }
+            if (roundaboutExitAngle < 1 || roundaboutExitAngle > 360) {
+                throw new IllegalArgumentException("Maneuver must include a valid exit angle");
+            }
+            this.mIsRoundaboutExitAngleSet = true;
+            this.mRoundaboutExitAngle = roundaboutExitAngle;
+            return this;
+        }
+
+        /**
+         * Constructs the {@link Maneuver} defined by this builder.
+         *
+         * @throws IllegalArgumentException if {@code type} includes an exit number and one has
+         *                                  not been set.
+         * @throws IllegalArgumentException if {@code type} includes an exit angle and one has
+         *                                  not been set.
+         */
+        @NonNull
+        public Maneuver build() {
+            if (isValidTypeWithExitNumber(mType) && !mIsRoundaboutExitNumberSet) {
+                throw new IllegalArgumentException("Maneuver missing roundaboutExitNumber");
+            }
+            if (isValidTypeWithExitAngle(mType) && !mIsRoundaboutExitAngleSet) {
+                throw new IllegalArgumentException("Maneuver missing roundaboutExitAngle");
+            }
+            return new Maneuver(mType, mRoundaboutExitNumber, mRoundaboutExitAngle, mIcon);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/MessageInfo.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/MessageInfo.java
new file mode 100644
index 0000000..490d300
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/MessageInfo.java
@@ -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.car.app.navigation.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.constraints.CarIconConstraints;
+import androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo;
+
+import java.util.Objects;
+
+/** Represents a message that can be shown in the {@link NavigationTemplate}. */
+public class MessageInfo implements NavigationInfo {
+    @Keep
+    @Nullable
+    private final CarText mTitle;
+    @Keep
+    @Nullable
+    private final CarText mText;
+    @Keep
+    @Nullable
+    private final CarIcon mImage;
+
+    /**
+     * Constructs a new builder of {@link MessageInfo}.
+     *
+     * @throws NullPointerException if {@code title} is {@code null}.
+     */
+    @NonNull
+    public static Builder builder(@NonNull CharSequence title) {
+        return new Builder(title);
+    }
+
+    @NonNull
+    public CarText getTitle() {
+        return requireNonNull(mTitle);
+    }
+
+    @Nullable
+    public CarText getText() {
+        return mText;
+    }
+
+    @Nullable
+    public CarIcon getImage() {
+        return mImage;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "MessageInfo";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mTitle, mText, mImage);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof MessageInfo)) {
+            return false;
+        }
+        MessageInfo otherInfo = (MessageInfo) other;
+
+        return Objects.equals(mTitle, otherInfo.mTitle)
+                && Objects.equals(mText, otherInfo.mText)
+                && Objects.equals(mImage, otherInfo.mImage);
+    }
+
+    private MessageInfo(Builder builder) {
+        mTitle = builder.mTitle;
+        mText = builder.mText;
+        mImage = builder.mImage;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private MessageInfo() {
+        mTitle = null;
+        mText = null;
+        mImage = null;
+    }
+
+    /** A builder of {@link MessageInfo}. */
+    public static final class Builder {
+        @Nullable
+        private CarText mTitle;
+        @Nullable
+        private CarText mText;
+        @Nullable
+        private CarIcon mImage;
+
+        private Builder(@NonNull CharSequence title) {
+            this.mTitle = CarText.create(requireNonNull(title));
+        }
+
+        /**
+         * Sets the title of the message.
+         *
+         * @throws NullPointerException if {@code message} is {@code null}.
+         */
+        @NonNull
+        public Builder setTitle(@NonNull CharSequence title) {
+            this.mTitle = CarText.create(requireNonNull(title));
+            return this;
+        }
+
+        /** Sets additional text on the message or {@code null} to not set any additional text. */
+        @NonNull
+        public Builder setText(@Nullable CharSequence text) {
+            this.mText = text == null ? null : CarText.create(text);
+            return this;
+        }
+
+        /**
+         * Sets the image to display along with the message, or {@code null} to not display an
+         * image.
+         */
+        @NonNull
+        public Builder setImage(@Nullable CarIcon image) {
+            CarIconConstraints.DEFAULT.validateOrThrow(image);
+            this.mImage = image;
+            return this;
+        }
+
+        /** Constructs the {@link MessageInfo} defined by this builder. */
+        @NonNull
+        public MessageInfo build() {
+            return new MessageInfo(this);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
new file mode 100644
index 0000000..22e6df5
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
@@ -0,0 +1,263 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_NAVIGATION;
+import static androidx.car.app.model.constraints.CarColorConstraints.UNCONSTRAINED;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.Screen;
+import androidx.car.app.SurfaceListener;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.Template;
+import androidx.car.app.utils.Logger;
+
+import java.util.Objects;
+
+// TODO(rampara): Update code reference to CarAppExtender is javadoc to link
+
+/**
+ * A template for showing navigation information.
+ *
+ * <p>This template has two independent sections which can be updated:
+ *
+ * <ul>
+ *   <li>Navigation information such as routing instructions or navigation-related messages.
+ *   <li>Travel estimates to the destination.
+ * </ul>
+ *
+ * <p>To update the template as the user navigates, call {@link Screen#invalidate} to provide the
+ * host with a new template with the updated information.
+ *
+ * <p>The template itself does not expose a drawing surface. In order to draw on the canvas, use
+ * {@link androidx.car.app.AppManager#setSurfaceListener(SurfaceListener)}.
+ *
+ * <p>See {@code androidx.car.app.notification.CarAppExtender} for how to show
+ * alerts with notifications. Frequent alert notifications distract the driver and are discouraged.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regard to template refreshes, as described in {@link Screen#getTemplate()}, this template
+ * supports any content changes as refreshes. This allows apps to interactively update the
+ * turn-by-turn instructions without the templates being counted against the template quota.
+ *
+ * <p>Further, this template is considered a view that the user will stay and consume contents from,
+ * and the host will reset the template quota once an app reaches this template.
+ *
+ * <p>In order to use this template your car app <b>MUST</b> declare that it uses the {@code
+ * androidx.car.app.NAVIGATION_TEMPLATES} permission in the manifest.
+ */
+public class NavigationTemplate implements Template {
+
+    /**
+     * Represents navigation information such as routing instructions or navigation-related
+     * messages.
+     */
+    public interface NavigationInfo {
+    }
+
+    @Keep
+    @Nullable
+    private final NavigationInfo mNavigationInfo;
+    @Keep
+    @Nullable
+    private final CarColor mBackgroundColor;
+    @Keep
+    @Nullable
+    private final TravelEstimate mDestinationTravelEstimate;
+    @Keep
+    @Nullable
+    private final ActionStrip mActionStrip;
+
+    /** Constructs a new builder of {@link NavigationTemplate}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @Nullable
+    public NavigationInfo getNavigationInfo() {
+        return mNavigationInfo;
+    }
+
+    @Nullable
+    public CarColor getBackgroundColor() {
+        return mBackgroundColor;
+    }
+
+    @Nullable
+    public TravelEstimate getDestinationTravelEstimate() {
+        return mDestinationTravelEstimate;
+    }
+
+    @NonNull
+    public ActionStrip getActionStrip() {
+        return requireNonNull(mActionStrip);
+    }
+
+    @Override
+    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+        requireNonNull(oldTemplate);
+
+        // Always allow updating on navigation templates.
+        return oldTemplate.getClass() == this.getClass();
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "NavigationTemplate";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mNavigationInfo, mBackgroundColor, mDestinationTravelEstimate,
+                mActionStrip);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof NavigationTemplate)) {
+            return false;
+        }
+        NavigationTemplate otherTemplate = (NavigationTemplate) other;
+
+        return Objects.equals(mNavigationInfo, otherTemplate.mNavigationInfo)
+                && Objects.equals(mBackgroundColor, otherTemplate.mBackgroundColor)
+                && Objects.equals(mDestinationTravelEstimate,
+                otherTemplate.mDestinationTravelEstimate)
+                && Objects.equals(mActionStrip, otherTemplate.mActionStrip);
+    }
+
+    @Override
+    public void checkPermissions(@NonNull Context context) {
+        CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
+    }
+
+    private NavigationTemplate(Builder builder) {
+        mNavigationInfo = builder.mNavigationInfo;
+        mBackgroundColor = builder.mBackgroundColor;
+        mDestinationTravelEstimate = builder.mDestinationTravelEstimate;
+        mActionStrip = builder.mActionStrip;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private NavigationTemplate() {
+        mNavigationInfo = null;
+        mBackgroundColor = null;
+        mDestinationTravelEstimate = null;
+        mActionStrip = null;
+    }
+
+    /** A builder of {@link NavigationTemplate}. */
+    public static final class Builder {
+        @Nullable
+        private NavigationInfo mNavigationInfo;
+        @Nullable
+        private CarColor mBackgroundColor;
+        @Nullable
+        private TravelEstimate mDestinationTravelEstimate;
+        private ActionStrip mActionStrip;
+
+        private Builder() {
+        }
+
+        /**
+         * Sets the navigation information to display on the template, or {@code null} to not
+         * display navigation information on top of the map.
+         */
+        @NonNull
+        public Builder setNavigationInfo(@Nullable NavigationInfo navigationInfo) {
+            this.mNavigationInfo = navigationInfo;
+            return this;
+        }
+
+        /**
+         * Sets the background color to use for the navigation information, or {@code null} to
+         * use the default.
+         *
+         * <p>The host may ignore this color and use a default color instead if the color does
+         * not pass the contrast requirements.
+         */
+        @NonNull
+        public Builder setBackgroundColor(@Nullable CarColor backgroundColor) {
+            if (backgroundColor != null) {
+                UNCONSTRAINED.validateOrThrow(backgroundColor);
+            }
+            this.mBackgroundColor = backgroundColor;
+            return this;
+        }
+
+        /**
+         * Sets the {@link TravelEstimate} to the final destination, or {@code null} to not show any
+         * travel estimate information.
+         */
+        @NonNull
+        public Builder setDestinationTravelEstimate(
+                @Nullable TravelEstimate destinationTravelEstimate) {
+            this.mDestinationTravelEstimate = destinationTravelEstimate;
+            return this;
+        }
+
+        /**
+         * Sets an {@link ActionStrip} with a list of template-scoped actions for this template.
+         *
+         * <h4>Requirements</h4>
+         *
+         * Besides {@link Action#APP_ICON} and {@link Action#BACK}, this template requires at
+         * least 1 and up to 4 {@link Action}s in its {@link ActionStrip}. Of the 4 allowed
+         * {@link Action}s, only one can contain a title as set via
+         * {@link Action.Builder#setTitle}. Otherwise, only {@link Action}s with icons are allowed.
+         *
+         * @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
+         *                                  requirements.
+         * @throws NullPointerException     if {@code actionStrip} is {@code null}.
+         */
+        @NonNull
+        public Builder setActionStrip(@NonNull ActionStrip actionStrip) {
+            ACTIONS_CONSTRAINTS_NAVIGATION.validateOrThrow(
+                    requireNonNull(actionStrip).getActions());
+            this.mActionStrip = actionStrip;
+            return this;
+        }
+
+        /**
+         * Constructs the {@link NavigationTemplate} defined by this builder.
+         *
+         * @throws IllegalStateException if an {@link ActionStrip} is not set on this template.
+         */
+        @NonNull
+        public NavigationTemplate build() {
+            if (mActionStrip == null) {
+                throw new IllegalStateException("Action strip for this template must be set.");
+            }
+            return new NavigationTemplate(this);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
new file mode 100644
index 0000000..51a304d
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
@@ -0,0 +1,350 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.Screen;
+import androidx.car.app.SurfaceListener;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.DistanceSpan;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.ModelUtils;
+import androidx.car.app.model.Place;
+import androidx.car.app.model.PlaceMarker;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.Toggle;
+import androidx.car.app.utils.Logger;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A template that supports showing a list of places alongside a custom drawn map.
+ *
+ * <p>The template itself does not expose a drawing surface. In order to draw on the canvas, use
+ * {@link androidx.car.app.AppManager#setSurfaceListener(SurfaceListener)}.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in {@link Screen#getTemplate()}, this template is
+ * considered a refresh of a previous one if:
+ *
+ * <ul>
+ *   <li>The template title has not changed, and
+ *   <li>The previous template is in a loading state (see {@link Builder#setIsLoading}, or the
+ *       number of rows and the string contents (title, texts, not counting spans) of each row
+ *       between the previous and new {@link ItemList}s have not changed.
+ * </ul>
+ *
+ * <p>In order to use this template your car app <b>MUST</b> declare that it uses the {@code
+ * androidx.car.app.NAVIGATION_TEMPLATES} permission in the manifest.
+ */
+public final class PlaceListNavigationTemplate implements Template {
+    @Keep
+    private final boolean mIsLoading;
+    @Keep
+    @Nullable
+    private final CarText mTitle;
+    @Keep
+    @Nullable
+    private final ItemList mItemList;
+    @Keep
+    @Nullable
+    private final Action mHeaderAction;
+    @Keep
+    @Nullable
+    private final ActionStrip mActionStrip;
+
+    /** Constructs a new builder of {@link PlaceListNavigationTemplate}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @Nullable
+    public CarText getTitle() {
+        return mTitle;
+    }
+
+    public boolean isLoading() {
+        return mIsLoading;
+    }
+
+    @Nullable
+    public ItemList getItemList() {
+        return mItemList;
+    }
+
+    @Nullable
+    public Action getHeaderAction() {
+        return mHeaderAction;
+    }
+
+    @Nullable
+    public ActionStrip getActionStrip() {
+        return mActionStrip;
+    }
+
+    @Override
+    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+        requireNonNull(oldTemplate);
+        if (oldTemplate.getClass() != this.getClass()) {
+            return false;
+        }
+
+        PlaceListNavigationTemplate old = (PlaceListNavigationTemplate) oldTemplate;
+        if (!Objects.equals(old.getTitle(), getTitle())) {
+            return false;
+        }
+
+        if (old.mIsLoading) {
+            // Transition from a previous loading state is allowed.
+            return true;
+        } else if (mIsLoading) {
+            // Transition to a loading state is disallowed.
+            return false;
+        }
+
+        return requireNonNull(mItemList).isRefresh(old.getItemList(), logger);
+    }
+
+    @Override
+    public void checkPermissions(@NonNull Context context) {
+        CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "PlaceListNavigationTemplate";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mTitle, mIsLoading, mItemList, mHeaderAction, mActionStrip);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof PlaceListNavigationTemplate)) {
+            return false;
+        }
+        PlaceListNavigationTemplate otherTemplate = (PlaceListNavigationTemplate) other;
+
+        return mIsLoading == otherTemplate.mIsLoading
+                && Objects.equals(mTitle, otherTemplate.mTitle)
+                && Objects.equals(mItemList, otherTemplate.mItemList)
+                && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+                && Objects.equals(mActionStrip, otherTemplate.mActionStrip);
+    }
+
+    private PlaceListNavigationTemplate(Builder builder) {
+        mTitle = builder.mTitle;
+        mIsLoading = builder.mIsLoading;
+        mItemList = builder.mItemList;
+        mHeaderAction = builder.mHeaderAction;
+        mActionStrip = builder.mActionStrip;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private PlaceListNavigationTemplate() {
+        mTitle = null;
+        mIsLoading = false;
+        mItemList = null;
+        mHeaderAction = null;
+        mActionStrip = null;
+    }
+
+    /** A builder of {@link PlaceListNavigationTemplate}. */
+    public static final class Builder {
+        @Nullable
+        private CarText mTitle;
+        private boolean mIsLoading;
+        @Nullable
+        private ItemList mItemList;
+        @Nullable
+        private Action mHeaderAction;
+        @Nullable
+        private ActionStrip mActionStrip;
+
+        /** Sets the {@link CharSequence} to show as title, or {@code null} to not show a title. */
+        @NonNull
+        public Builder setTitle(@Nullable CharSequence title) {
+            this.mTitle = title == null ? null : CarText.create(title);
+            return this;
+        }
+
+        /**
+         * Sets whether the template is in a loading state.
+         *
+         * <p>If set to {@code true}, the UI will show a loading indicator where the list content
+         * would be otherwise. The caller is expected to call
+         * {@link androidx.car.app.Screen#invalidate()} and send the new template content to the
+         * host
+         * once the data is ready. If set to {@code false}, the UI shows the {@link ItemList}
+         * contents added via {@link #setItemList}.
+         */
+        // TODO(rampara): Consider renaming to setLoading()
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setIsLoading(boolean isLoading) {
+            this.mIsLoading = isLoading;
+            return this;
+        }
+
+        /**
+         * Sets the {@link Action} that will be displayed in the header of the template, or
+         * {@code null} to not display an action.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template only supports either either one of {@link Action#APP_ICON} and {@link
+         * Action#BACK} as a header {@link Action}.
+         *
+         * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setHeaderAction(@Nullable Action headerAction) {
+            ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+                    headerAction == null ? Collections.emptyList()
+                            : Collections.singletonList(headerAction));
+            this.mHeaderAction = headerAction;
+            return this;
+        }
+
+        /**
+         * Sets an {@link ItemList} to show in the list view along with the map, or {@code null}
+         * to not display a list.
+         *
+         * <p>To show a marker corresponding to a point of interest represented by a row, set the
+         * {@link Place} instance via {@link Row.Builder#setMetadata}. The host will render the
+         * {@link PlaceMarker} in the list view as the row become visible. The app should
+         * synchronize with the list's behavior by rendering the same marker on the map surface.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 6 {@link Row}s in the {@link ItemList}. The host will
+         * ignore any items over that limit. The list itself cannot be selectable as set via {@link
+         * ItemList.Builder#setSelectable}. Each {@link Row} can add up to 2 lines of texts via
+         * {@link Row.Builder#addText} and cannot contain a {@link Toggle}.
+         *
+         * <p>Images of type {@link Row#IMAGE_TYPE_LARGE} are not allowed in this template.
+         *
+         * <p>Rows are not allowed to have both and an image and a place marker.
+         *
+         * <p>All non-browsable rows must have a {@link DistanceSpan} attached to either its
+         * title or texts, to indicate the distance of the point of interest from the current
+         * location. Where in the title or text the span is attached to is up to the app.
+         *
+         * @throws IllegalArgumentException if {@code itemList} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setItemList(@Nullable ItemList itemList) {
+            if (itemList != null) {
+                List<Object> items = itemList.getItems();
+                ROW_LIST_CONSTRAINTS_SIMPLE.validateOrThrow(itemList);
+                ModelUtils.validateAllNonBrowsableRowsHaveDistance(items);
+                ModelUtils.validateAllRowsHaveOnlySmallImages(items);
+                ModelUtils.validateNoRowsHaveBothMarkersAndImages(items);
+            }
+            this.mItemList = itemList;
+
+            return this;
+        }
+
+        /**
+         * Sets an {@link ItemList} for the template. This method does not enforce the
+         * template's requirements and is only intended for testing purposes.
+         */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @VisibleForTesting
+        @NonNull
+        public Builder setItemListForTesting(@Nullable ItemList itemList) {
+            this.mItemList = itemList;
+            return this;
+        }
+
+        /**
+         * Sets the {@link ActionStrip} for this template, or {@code null} to not show an {@link
+         * ActionStrip}.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
+         * {@link Action}s, one of them can contain a title as set via
+         * {@link Action.Builder#setTitle}. Otherwise, only {@link Action}s with icons are allowed.
+         *
+         * @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+            ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+                    actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+            this.mActionStrip = actionStrip;
+            return this;
+        }
+
+        /**
+         * Constructs the template defined by this builder.
+         *
+         * <h4>Requirements</h4>
+         *
+         * Either a header {@link Action} or title must be set on the template.
+         *
+         * @throws IllegalArgumentException if the template is in a loading state but the list is
+         *                                  set, or vice-versa.
+         * @throws IllegalStateException    if the template does not have either a title or header
+         *                                  {@link Action} set.
+         */
+        @NonNull
+        public PlaceListNavigationTemplate build() {
+            boolean hasList = mItemList != null;
+            if (mIsLoading == hasList) {
+                throw new IllegalArgumentException(
+                        "Template is in a loading state but a list is set, or vice versa.");
+            }
+
+            if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+                throw new IllegalStateException("Either the title or header action must be set");
+            }
+
+            return new PlaceListNavigationTemplate(this);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
new file mode 100644
index 0000000..aae7e95
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
@@ -0,0 +1,403 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
+import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.Screen;
+import androidx.car.app.SurfaceListener;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.ModelUtils;
+import androidx.car.app.model.OnClickListener;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.Toggle;
+import androidx.car.app.utils.Logger;
+
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * A template that supports showing a list of routes alongside a custom drawn map.
+ *
+ * <p>The list must have its {@link
+ * androidx.car.app.model.ItemList.OnSelectedListener} set, and the template
+ * must have its navigate action set (see {@link Builder#setNavigateAction}). These are used in
+ * conjunction to inform the app that:
+ *
+ * <ol>
+ *   <li>A route has been selected. The app should also highlight the route on the map surface.
+ *   <li>A navigate action has been triggered. The app should begin navigation using the selected
+ *       route.
+ * </ol>
+ *
+ * <p>The template itself does not expose a drawing surface. In order to draw on the canvas, use
+ * {@link androidx.car.app.AppManager#setSurfaceListener(SurfaceListener)}.
+ *
+ * <h4>Template Restrictions</h4>
+ *
+ * In regards to template refreshes, as described in {@link Screen#getTemplate()}, this template is
+ * considered a refresh of a previous one if:
+ *
+ * <ul>
+ *   <li>The template title has not changed, and
+ *   <li>The previous template is in a loading state (see {@link Builder#setIsLoading}, or the
+ *       number of rows and the string contents (title, texts, not counting spans) of each row
+ *       between the previous and new {@link ItemList}s have not changed.
+ * </ul>
+ *
+ * <p>In order to use this template your car app <b>MUST</b> declare that it uses the {@code
+ * androidx.car.app.NAVIGATION_TEMPLATES} permission in the manifest.
+ */
+public final class RoutePreviewNavigationTemplate implements Template {
+    @Keep
+    private final boolean mIsLoading;
+    @Keep
+    @Nullable
+    private final CarText mTitle;
+    @Keep
+    @Nullable
+    private final Action mNavigateAction;
+    @Keep
+    @Nullable
+    private final ItemList mItemList;
+    @Keep
+    @Nullable
+    private final Action mHeaderAction;
+    @Keep
+    @Nullable
+    private final ActionStrip mActionStrip;
+
+    /** Constructs a new builder of {@link RoutePreviewNavigationTemplate}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns the {@link CarText} that should be used as the title in the template.
+     */
+    @Nullable
+    public CarText getTitle() {
+        return mTitle;
+    }
+
+    public boolean isLoading() {
+        return mIsLoading;
+    }
+
+    @Nullable
+    public Action getNavigateAction() {
+        return mNavigateAction;
+    }
+
+    @Nullable
+    public ItemList getItemList() {
+        return mItemList;
+    }
+
+    @Nullable
+    public Action getHeaderAction() {
+        return mHeaderAction;
+    }
+
+    @Nullable
+    public ActionStrip getActionStrip() {
+        return mActionStrip;
+    }
+
+    @Override
+    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
+        if (oldTemplate.getClass() != this.getClass()) {
+            return false;
+        }
+
+        RoutePreviewNavigationTemplate old = (RoutePreviewNavigationTemplate) oldTemplate;
+        if (!Objects.equals(old.getTitle(), getTitle())) {
+            return false;
+        }
+
+        if (old.mIsLoading) {
+            // Transition from a previous loading state is allowed.
+            return true;
+        } else if (mIsLoading) {
+            // Transition to a loading state is disallowed.
+            return false;
+        }
+
+        return requireNonNull(mItemList).isRefresh(old.getItemList(), logger);
+    }
+
+    @Override
+    public void checkPermissions(@NonNull Context context) {
+        CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "RoutePreviewNavigationTemplate";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mTitle, mIsLoading, mNavigateAction, mItemList, mHeaderAction,
+                mActionStrip);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof RoutePreviewNavigationTemplate)) {
+            return false;
+        }
+        RoutePreviewNavigationTemplate otherTemplate = (RoutePreviewNavigationTemplate) other;
+
+        return mIsLoading == otherTemplate.mIsLoading
+                && Objects.equals(mTitle, otherTemplate.mTitle)
+                && Objects.equals(mNavigateAction, otherTemplate.mNavigateAction)
+                && Objects.equals(mItemList, otherTemplate.mItemList)
+                && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
+                && Objects.equals(mActionStrip, otherTemplate.mActionStrip);
+    }
+
+    private RoutePreviewNavigationTemplate(Builder builder) {
+        mTitle = builder.mTitle;
+        mIsLoading = builder.mIsLoading;
+        mNavigateAction = builder.mNavigateAction;
+        mItemList = builder.mItemList;
+        mHeaderAction = builder.mHeaderAction;
+        mActionStrip = builder.mActionStrip;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private RoutePreviewNavigationTemplate() {
+        mTitle = null;
+        mIsLoading = false;
+        mNavigateAction = null;
+        mItemList = null;
+        mHeaderAction = null;
+        mActionStrip = null;
+    }
+
+    /** A builder of {@link RoutePreviewNavigationTemplate}. */
+    public static final class Builder {
+        @Nullable
+        private CarText mTitle;
+        private boolean mIsLoading;
+        @Nullable
+        private Action mNavigateAction;
+        @Nullable
+        private ItemList mItemList;
+        @Nullable
+        private Action mHeaderAction;
+        @Nullable
+        private ActionStrip mActionStrip;
+
+        /** Sets the {@link CharSequence} to show as title, or {@code null} to not show a title. */
+        @NonNull
+        public Builder setTitle(@Nullable CharSequence title) {
+            this.mTitle = title == null ? null : CarText.create(title);
+            return this;
+        }
+
+        /**
+         * Sets whether the template is in a loading state.
+         *
+         * <p>If set to {@code true}, the UI will show a loading indicator where the list content
+         * would be otherwise. The caller is expected to call
+         * {@link androidx.car.app.Screen#invalidate()} and send the new template content to the
+         * host
+         * once the data is ready. If set to {@code false}, the UI shows the {@link ItemList}
+         * contents added via {@link #setItemList}.
+         */
+        // TODO(rampara): Consider renaming to setLoading()
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setIsLoading(boolean isLoading) {
+            this.mIsLoading = isLoading;
+            return this;
+        }
+
+        /**
+         * Sets the {@link Action} that will be displayed in the header of the template, or
+         * {@code null} to now display an action.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template only supports either either one of {@link Action#APP_ICON} and {@link
+         * Action#BACK} as a header {@link Action}.
+         *
+         * @throws IllegalArgumentException if {@code headerAction} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setHeaderAction(@Nullable Action headerAction) {
+            ACTIONS_CONSTRAINTS_HEADER.validateOrThrow(
+                    headerAction == null ? Collections.emptyList()
+                            : Collections.singletonList(headerAction));
+            this.mHeaderAction = headerAction;
+            return this;
+        }
+
+        /**
+         * Sets the {@link Action} to allow users to request navigation using the currently selected
+         * route.
+         *
+         * <p>This should not be {@code null} if the template is not in a loading state (see
+         * #setIsLoading}), and the {@link Action}'s title must be set.
+         *
+         * @throws NullPointerException     if {@code navigateAction} is {@code null}.
+         * @throws IllegalArgumentException if {@code navigateAction}'s title is {@code null} or
+         *                                  empty.
+         */
+        @NonNull
+        public Builder setNavigateAction(@NonNull Action navigateAction) {
+            if (CarText.isNullOrEmpty(requireNonNull(navigateAction).getTitle())) {
+                throw new IllegalArgumentException("The Action's title cannot be null or empty");
+            }
+
+            this.mNavigateAction = requireNonNull(navigateAction);
+
+            return this;
+        }
+
+        /**
+         * Sets an {@link ItemList} to show route options in a list view along with the map.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 3 {@link Row}s in the {@link ItemList}. The host will
+         * ignore any items over that limit. The list must have an {@link OnClickListener} set. Each
+         * {@link Row} can add up to 2 lines of texts via {@link Row.Builder#addText} and cannot
+         * contain a {@link Toggle}.
+         *
+         * <p>Images of type {@link Row#IMAGE_TYPE_LARGE} are not allowed in this template.
+         *
+         * <p>All rows must have either a {@link
+         * androidx.car.app.model.DistanceSpan} or a {@link
+         * androidx.car.app.model.DurationSpan} attached to either its title or texts, to
+         * indicate an estimate trip distance or duration for the route it represents. Where in
+         * the title or text these spans are attached to is up to the app.
+         *
+         * @throws IllegalArgumentException if {@code itemList} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setItemList(@Nullable ItemList itemList) {
+            if (itemList != null) {
+                ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW.validateOrThrow(itemList);
+                ModelUtils.validateAllRowsHaveDistanceOrDuration(itemList.getItems());
+                ModelUtils.validateAllRowsHaveOnlySmallImages(itemList.getItems());
+
+                if (!itemList.getItems().isEmpty() && itemList.getOnSelectedListener() == null) {
+                    throw new IllegalArgumentException(
+                            "The OnSelectedListener must be set for the route list");
+                }
+            }
+            this.mItemList = itemList;
+
+            return this;
+        }
+
+        /**
+         * Sets an {@link ItemList} for the template. This method does not enforce the
+         * template's requirements and is only intended for testing purposes.
+         */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @VisibleForTesting
+        @NonNull
+        public Builder setItemListForTesting(@Nullable ItemList itemList) {
+            this.mItemList = itemList;
+            return this;
+        }
+
+        /**
+         * Sets the {@link ActionStrip} for this template, or {@code null} to not show an {@link
+         * ActionStrip}.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2
+         * allowed {@link Action}s, one of them can contain a title as set via
+         * {@link Action.Builder#setTitle}. Otherwise, only {@link Action}s with icons are allowed.
+         *
+         * @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
+         *                                  requirements.
+         */
+        @NonNull
+        public Builder setActionStrip(@Nullable ActionStrip actionStrip) {
+            ACTIONS_CONSTRAINTS_SIMPLE.validateOrThrow(
+                    actionStrip == null ? Collections.emptyList() : actionStrip.getActions());
+            this.mActionStrip = actionStrip;
+            return this;
+        }
+
+        /**
+         * Constructs the template defined by this builder.
+         *
+         * <h4>Requirements</h4>
+         *
+         * Either a header {@link Action} or title must be set on the template.
+         *
+         * @throws IllegalStateException if the template is in a loading state but the list is
+         *                               set, or vice-versa.
+         * @throws IllegalStateException if the template is not loading and the navigation action
+         *                               is not set.
+         * @throws IllegalStateException if the template does not have either a title or header
+         *                               {@link Action} set.
+         */
+        @NonNull
+        public RoutePreviewNavigationTemplate build() {
+            boolean hasList = mItemList != null;
+            if (mIsLoading == hasList) {
+                throw new IllegalStateException(
+                        "Template is in a loading state but a list is set, or vice versa.");
+            }
+
+            if (!mIsLoading) {
+                if (mNavigateAction == null) {
+                    throw new IllegalStateException(
+                            "The navigation action cannot be null when the list is not in a "
+                                    + "loading state");
+                }
+            }
+
+            if (CarText.isNullOrEmpty(mTitle) && mHeaderAction == null) {
+                throw new IllegalStateException("Either the title or header action must be set");
+            }
+
+            return new RoutePreviewNavigationTemplate(this);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java
new file mode 100644
index 0000000..1de529f
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java
@@ -0,0 +1,265 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Distance;
+import androidx.car.app.model.constraints.CarIconConstraints;
+import androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo;
+
+import java.util.Objects;
+
+/**
+ * Represents routing information that can be shown in the {@link NavigationTemplate} during
+ * navigation
+ */
+public class RoutingInfo implements NavigationInfo {
+    @Keep
+    @Nullable
+    private final Step mCurrentStep;
+    @Keep
+    @Nullable
+    private final Distance mCurrentDistance;
+    @Keep
+    @Nullable
+    private final Step mNextStep;
+    @Keep
+    @Nullable
+    private final CarIcon mJunctionImage;
+    @Keep
+    private final boolean mIsLoading;
+
+    /** Constructs a new builder of {@link RoutingInfo}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @Nullable
+    public Step getCurrentStep() {
+        return requireNonNull(mCurrentStep);
+    }
+
+    @Nullable
+    public Distance getCurrentDistance() {
+        return requireNonNull(mCurrentDistance);
+    }
+
+    @Nullable
+    public Step getNextStep() {
+        return mNextStep;
+    }
+
+    @Nullable
+    public CarIcon getJunctionImage() {
+        return mJunctionImage;
+    }
+
+    public boolean isLoading() {
+        return mIsLoading;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "RoutingInfo";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mCurrentStep, mCurrentDistance, mNextStep, mJunctionImage, mIsLoading);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof RoutingInfo)) {
+            return false;
+        }
+        RoutingInfo otherInfo = (RoutingInfo) other;
+
+        return mIsLoading == otherInfo.mIsLoading
+                && Objects.equals(mCurrentStep, otherInfo.mCurrentStep)
+                && Objects.equals(mCurrentDistance, otherInfo.mCurrentDistance)
+                && Objects.equals(mNextStep, otherInfo.mNextStep)
+                && Objects.equals(mJunctionImage, otherInfo.mJunctionImage);
+    }
+
+    private RoutingInfo(Builder builder) {
+        mCurrentStep = builder.mCurrentStep;
+        mCurrentDistance = builder.mCurrentDistance;
+        mNextStep = builder.mNextStep;
+        mJunctionImage = builder.mJunctionImage;
+        mIsLoading = builder.mIsLoading;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private RoutingInfo() {
+        mCurrentStep = null;
+        mCurrentDistance = null;
+        mNextStep = null;
+        mJunctionImage = null;
+        mIsLoading = false;
+    }
+
+    /** A builder of {@link RoutingInfo}. */
+    public static final class Builder {
+        @Nullable
+        private Step mCurrentStep;
+        @Nullable
+        private Distance mCurrentDistance;
+        @Nullable
+        private Step mNextStep;
+        @Nullable
+        private CarIcon mJunctionImage;
+        private boolean mIsLoading;
+
+        private Builder() {
+        }
+
+        /**
+         * Sets the current {@link Step} and {@link Distance} to display in the template.
+         *
+         * <p>A {@link Step} with a {@link Maneuver} of type {@link Maneuver#TYPE_UNKNOWN} will
+         * shown here with the given icon.
+         *
+         * <h4>Image Sizing Guidance</h4>
+         *
+         * Images in the cue of the {@link Step} object, set with {@link Step.Builder#setCue}, can
+         * contain image spans. If necessary, those images in the spans will be scaled down to fit
+         * within a 108 x 36 dp bounding box, while preserving their aspect ratio.
+         *
+         * <p>See {@link CarIcon} for more details related to providing icon and image resources
+         * that work with different car screen pixel densities.
+         */
+        @NonNull
+        public Builder setCurrentStep(@NonNull Step currentStep,
+                @NonNull Distance currentDistance) {
+            this.mCurrentStep = requireNonNull(currentStep);
+            this.mCurrentDistance = requireNonNull(currentDistance);
+            return this;
+        }
+
+        /**
+         * Sets the next {@link Step} or {@code null} to not display it.
+         *
+         * <h4>Image Sizing Guidance</h4>
+         *
+         * Images in the cue of the {@link Step} object, set with {@link Step.Builder#setCue}, can
+         * contain image spans. If necessary, those images in the spans will be scaled down to fit
+         * within a 108 x 32 dp bounding box, while preserving their aspect ratio.
+         *
+         * <p>See {@link CarIcon} for more details related to providing icon and image resources
+         * that work with different car screen pixel densities.
+         */
+        @NonNull
+        public Builder setNextStep(@Nullable Step nextStep) {
+            this.mNextStep = nextStep;
+            return this;
+        }
+
+        /**
+         * Sets an image of a junction for the maneuver or {@code null} to not show a junction
+         * image.
+         *
+         * <p>For example, a photo-realistic view of the upcoming junction that the driver can
+         * see when executing the maneuver.
+         *
+         * <h4>Image Sizing Guidance</h4>
+         *
+         * The image may be scaled down to fit a rectangle of 320 x 200 dp while preserving the
+         * aspect ratio. On smaller screens the junction image may result in the hiding of the
+         * {@link Lane}s, {@link TravelEstimate} or next {@link Step}. The aspect ratio should be
+         * greater than or equal to 1.6 in order to fit the horizontal space fully.
+         *
+         * <p>See {@link CarIcon} for more details related to providing icon and image resources
+         * that work with different car screen pixel densities.
+         */
+        @NonNull
+        public Builder setJunctionImage(@Nullable CarIcon junctionImage) {
+            CarIconConstraints.DEFAULT.validateOrThrow(junctionImage);
+            this.mJunctionImage = junctionImage;
+            return this;
+        }
+
+        /**
+         * Sets whether the {@link RoutingInfo} is in a loading state.
+         *
+         * <p>If set to {@code true}, the UI will show a loading indicator, and adding any other
+         * routing info will throw an {@link IllegalArgumentException}. The caller is expected to
+         * call {@link androidx.car.app.Screen#invalidate()} and send the new template content
+         * to the host once the data is ready. If set to {@code false}, the UI shows the actual
+         * routing info.
+         *
+         * @see #build
+         */
+        // TODO(rampara): Consider renaming to setLoading()
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setIsLoading(boolean isLoading) {
+            this.mIsLoading = isLoading;
+            return this;
+        }
+
+        /**
+         * Constructs the {@link RoutingInfo} defined by this builder.
+         *
+         * <h4>Requirements</h4>
+         *
+         * The {@link RoutingInfo} can be in a loading state by passing {@code true} to {@link
+         * #setIsLoading(boolean)}, in which case no other fields may be set. Otherwise, the current
+         * step and distance must be set. If the lane information is set with {@link
+         * Step.Builder#addLane(Lane)}, then the lane image must also be set with {@link
+         * Step.Builder#setLanesImage(CarIcon)}.
+         *
+         * @throws IllegalStateException if the {@link RoutingInfo} does not meet the template's
+         *                               requirements.
+         */
+        @NonNull
+        public RoutingInfo build() {
+            Step current = mCurrentStep;
+            Distance distance = mCurrentDistance;
+
+            if (mIsLoading) {
+                if (current != null || distance != null || mNextStep != null
+                        || mJunctionImage != null) {
+                    throw new IllegalStateException(
+                            "The routing info is set to loading but is not empty");
+                }
+            } else {
+                if (current == null || distance == null) {
+                    throw new IllegalStateException(
+                            "Current step and distance must be set during the navigating state");
+                }
+                if (!current.getLanes().isEmpty() && current.getLanesImage() == null) {
+                    // TODO(b/154660041): Remove restriction when lane image can be draw from
+                    //  lane info.
+                    throw new IllegalStateException(
+                            "Current step must have a lanes image if the lane information is set.");
+                }
+            }
+            return new RoutingInfo(this);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
new file mode 100644
index 0000000..7a1c100
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
@@ -0,0 +1,324 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.utils.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a step that the driver should take in order to remain on the current navigation route.
+ *
+ * <p>Example of steps are turning onto a street, taking a highway exit and merging onto a different
+ * highway, or continuing straight through a roundabout.
+ */
+public final class Step {
+    @Keep
+    @Nullable
+    private final Maneuver mManeuver;
+    @Keep
+    private final List<Lane> mLanes;
+    @Keep
+    @Nullable
+    private final CarIcon mLanesImage;
+    @Keep
+    @Nullable
+    private final CarText mCue;
+    @Keep
+    @Nullable
+    private final CarText mRoad;
+
+    /**
+     * Constructs a new builder of {@link Step} with a cue.
+     *
+     * <p>A cue must always be set when the step is created and is used as a fallback when {@link
+     * Maneuver} is not set or is unavailable.
+     *
+     * @throws NullPointerException if {@code cue} is {@code null}.
+     * @see Builder#setCue(CharSequence)
+     */
+    @NonNull
+    public static Builder builder(@NonNull CharSequence cue) {
+        return new Builder(requireNonNull(cue));
+    }
+
+    /**
+     * Returns a new {@link Builder} instance configured with the same data as this {@link Step}
+     * instance.
+     */
+    @NonNull
+    public Builder newBuilder() {
+        return new Builder(this);
+    }
+
+    @Nullable
+    public Maneuver getManeuver() {
+        return mManeuver;
+    }
+
+    @NonNull
+    public List<Lane> getLanes() {
+        return CollectionUtils.emptyIfNull(mLanes);
+    }
+
+    @Nullable
+    public CarIcon getLanesImage() {
+        return mLanesImage;
+    }
+
+    @Nullable
+    public CarText getCue() {
+        return mCue;
+    }
+
+    @Nullable
+    public CarText getRoad() {
+        return mRoad;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[maneuver: "
+                + mManeuver
+                + ", lane count: "
+                + (mLanes != null ? mLanes.size() : 0)
+                + ", lanes image: "
+                + mLanesImage
+                + ", cue: "
+                + CarText.toShortString(mCue)
+                + ", road: "
+                + CarText.toShortString(mRoad)
+                + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mManeuver, mLanes, mLanesImage, mCue, mRoad);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof Step)) {
+            return false;
+        }
+
+        Step otherStep = (Step) other;
+        return Objects.equals(mManeuver, otherStep.mManeuver)
+                && Objects.equals(mLanes, otherStep.mLanes)
+                && Objects.equals(mLanesImage, otherStep.mLanesImage)
+                && Objects.equals(mCue, otherStep.mCue)
+                && Objects.equals(mRoad, otherStep.mRoad);
+    }
+
+    private Step(
+            @Nullable Maneuver maneuver,
+            List<Lane> lanes,
+            @Nullable CarIcon lanesImage,
+            @Nullable CarText cue,
+            @Nullable CarText road) {
+        this.mManeuver = maneuver;
+        this.mLanes = new ArrayList<>(lanes);
+        this.mLanesImage = lanesImage;
+        this.mCue = cue;
+        this.mRoad = road;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private Step() {
+        mManeuver = null;
+        mLanes = Collections.emptyList();
+        mLanesImage = null;
+        mCue = null;
+        mRoad = null;
+    }
+
+    /** A builder of {@link Step}. */
+    public static final class Builder {
+        private final List<Lane> mLanes = new ArrayList<>();
+        @Nullable
+        private Maneuver mManeuver;
+        @Nullable
+        private CarIcon mLanesImage;
+        private CarText mCue;
+        @Nullable
+        private CarText mRoad;
+
+        private Builder(CharSequence cue) {
+            this.mCue = CarText.create(cue);
+        }
+
+        private Builder(Step step) {
+            this.mManeuver = step.mManeuver;
+            this.mLanes.clear();
+            this.mLanes.addAll(step.mLanes);
+            this.mLanesImage = step.mLanesImage;
+            this.mCue = requireNonNull(step.mCue);
+            this.mRoad = step.mRoad;
+        }
+
+        /**
+         * Sets the maneuver to be performed on this step or {@code null} if this step doesn't
+         * involve a
+         * maneuver.
+         */
+        @NonNull
+        public Builder setManeuver(@Nullable Maneuver maneuver) {
+            this.mManeuver = maneuver;
+            return this;
+        }
+
+        /**
+         * Adds the information of a single road lane at the point where the driver should
+         * execute this step.
+         *
+         * <p>Lane information is primarily used when the step is passed to the vehicle cluster
+         * or heads up displays. Some vehicles may not use the information at all. The navigation
+         * template primarily uses the lanes image provided in {@link #setLanesImage}.
+         *
+         * <p>Lanes are displayed from left to right.
+         */
+        @NonNull
+        public Builder addLane(@NonNull Lane lane) {
+            requireNonNull(lane);
+            mLanes.add(lane);
+            return this;
+        }
+
+        /**
+         * Clears any lanes that may have been added with {@link #addLane(Lane)} up to this
+         * point.
+         */
+        @NonNull
+        public Builder clearLanes() {
+            mLanes.clear();
+            return this;
+        }
+
+        /**
+         * Sets an image representing all the lanes or {@code null} if no lanes image is available.
+         *
+         * <p>This image takes priority over {@link Lane}s that may have been added with {@link
+         * #addLane}. If an image is added for the lanes with this method then corresponding lane
+         * data using {@link #addLane} must also have been added in case it is shown on a display
+         * with limited resources such as the car cluster or heads-up display (HUD).
+         *
+         * <p>This image should ideally have a transparent background.
+         *
+         * <h4>Image Sizing Guidance</h4>
+         *
+         * The provided image should have a maximum size of 294 x 44 dp. If the image exceeds this
+         * maximum size in either one of the dimensions, it will be scaled down and centered
+         * inside the bounding box while preserving the aspect ratio.
+         *
+         * <p>See {@link CarIcon} for more details related to providing icon and image resources
+         * that work with different car screen pixel densities.
+         */
+        @NonNull
+        public Builder setLanesImage(@Nullable CarIcon lanesImage) {
+            this.mLanesImage = lanesImage;
+            return this;
+        }
+
+        /**
+         * Sets a text description of this maneuver.
+         *
+         * <p>Must always be set when the step is created and is used as a fallback when {@link
+         * Maneuver} is not set or is unavailable.
+         *
+         * <p>For example "Turn left", "Make a U-Turn", "Sharp Right", or "Take the exit using
+         * the left lane"
+         *
+         * <p>The {@code cue} string can contain images that replace spans of text by using {@link
+         * androidx.car.app.model.CarIconSpan}.
+         *
+         * <p>In the following example, the "520" text is replaced with an icon:
+         *
+         * <pre>{@code
+         * SpannableString string = new SpannableString("Turn right on 520 East");
+         * string.setSpan(textWithImage.setSpan(
+         *     CarIconSpan.create(CarIcon.of(
+         *         IconCompat.createWithResource(getCarContext(), R.drawable.ic_520_highway))),
+         *         14, 17, SPAN_INCLUSIVE_EXCLUSIVE));
+         * }</pre>
+         *
+         * <p>The host may choose to display the string without the images, so it is important
+         * that the string content is readable without the images. This may be the case, for
+         * example, if the string is sent to a cluster display that does not support images, or
+         * if the host limits the number of images that may be allowed for one line of text.
+         *
+         * <h4>Image Sizing Guidance</h4>
+         *
+         * The size these images will be displayed at varies depending on where the {@link Step}
+         * object is used. Refer to the documentation of those APIs for details.
+         *
+         * <p>See {@link CarIcon} for more details related to providing icon and image resources
+         * that work with different car screen pixel densities.
+         *
+         * @throws NullPointerException if {@code cue} is {@code null}
+         */
+        @NonNull
+        public Builder setCue(@NonNull CharSequence cue) {
+            this.mCue = CarText.create(requireNonNull(cue));
+            return this;
+        }
+
+        /**
+         * Sets a text description of the road for the step or {@code null} if unknown.
+         *
+         * <p>This value is primarily used for vehicle cluster and heads-up displays and may not
+         * appear
+         * in the navigation template.
+         *
+         * <p>For example, a {@link Step} for a left turn might provide "State Street" for the road.
+         *
+         * @throws NullPointerException if {@code destinations} is {@code null}
+         */
+        @NonNull
+        public Builder setRoad(@NonNull CharSequence road) {
+            this.mRoad = CarText.create(requireNonNull(road));
+            return this;
+        }
+
+        /**
+         * Constructs the {@link Step} defined by this builder.
+         *
+         * @throws IllegalStateException if {@code lanesImage} was set but no lanes were added.
+         */
+        @NonNull
+        public Step build() {
+            if (mLanesImage != null && mLanes.isEmpty()) {
+                throw new IllegalStateException(
+                        "A step must have lane data when the lanes image is set.");
+            }
+            return new Step(mManeuver, mLanes, mLanesImage, mCue, mRoad);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
new file mode 100644
index 0000000..cda7135
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
@@ -0,0 +1,322 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.DateTimeWithZone;
+import androidx.car.app.model.Distance;
+import androidx.car.app.model.constraints.CarColorConstraints;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.util.Objects;
+
+/**
+ * Represents the travel estimates to a destination of a trip or for a trip segment, including the
+ * remaining time and distance to the destination.
+ */
+@SuppressWarnings("MissingSummary")
+public final class TravelEstimate {
+    @Keep
+    @Nullable
+    private final Distance mRemainingDistance;
+    @Keep
+    private final long mRemainingTimeSeconds;
+    @Keep
+    @Nullable
+    private final DateTimeWithZone mArrivalTimeAtDestination;
+    @Keep
+    private final CarColor mRemainingTimeColor;
+    @Keep
+    private final CarColor mRemainingDistanceColor;
+
+    /**
+     * Returns a new instance of a {@link TravelEstimate} for the given time and distance
+     * parameters.
+     *
+     * @param remainingDistance        The estimated remaining {@link Distance} until arriving at
+     *                                 the destination.
+     * @param remainingTimeSeconds     The estimated time remaining until arriving at the
+     *                                 destination, in seconds.
+     * @param arrivalTimeAtDestination The arrival time with the time zone information provided
+     *                                 for the destination.
+     * @throws IllegalArgumentException if {@code remainingTimeSeconds} is a negative value.
+     * @throws NullPointerException     if {@code remainingDistance} is {@code null}
+     * @throws NullPointerException     if {@code arrivalTimeAtDestination} is {@code null}
+     */
+    @NonNull
+    public static TravelEstimate create(
+            @NonNull Distance remainingDistance,
+            long remainingTimeSeconds,
+            @NonNull DateTimeWithZone arrivalTimeAtDestination) {
+        return builder(remainingDistance, remainingTimeSeconds, arrivalTimeAtDestination).build();
+    }
+
+    /**
+     * Returns a new instance of a {@link TravelEstimate} for the given time and distance
+     * parameters.
+     *
+     * @param remainingDistance        The estimated remaining {@link Distance} until arriving at
+     *                                 the destination.
+     * @param remainingTime            The estimated time remaining until arriving at the
+     *                                 destination.
+     * @param arrivalTimeAtDestination The arrival time with the time zone information provided for
+     *                                 the destination.
+     * @throws IllegalArgumentException if {@code remainingTime} contains a negative duration.
+     * @throws NullPointerException     if {@code remainingDistance} is {@code null}
+     * @throws NullPointerException     if {@code remainingTime} is {@code null}
+     * @throws NullPointerException     if {@code arrivalTimeAtDestination} is {@code null}
+     */
+    @RequiresApi(26)
+    @SuppressWarnings("AndroidJdkLibsChecker")
+    @NonNull
+    public static TravelEstimate create(
+            @NonNull Distance remainingDistance,
+            @NonNull Duration remainingTime,
+            @NonNull ZonedDateTime arrivalTimeAtDestination) {
+        return builder(remainingDistance, remainingTime, arrivalTimeAtDestination).build();
+    }
+
+    /**
+     * Constructs a new builder of {@link TravelEstimate}.
+     *
+     * @param remainingDistance        The estimated remaining {@link Distance} until arriving at
+     *                                 the destination.
+     * @param remainingTimeSeconds     The estimated time remaining until arriving at the
+     *                                 destination, in seconds.
+     * @param arrivalTimeAtDestination The arrival time with the time zone information provided
+     *                                 for the destination.
+     * @throws IllegalArgumentException if {@code remainingTimeSeconds} is a negative value.
+     * @throws NullPointerException     if {@code remainingDistance} is {@code null}
+     * @throws NullPointerException     if {@code arrivalTimeAtDestination} is {@code null}
+     */
+    @NonNull
+    public static Builder builder(
+            @NonNull Distance remainingDistance,
+            long remainingTimeSeconds,
+            @NonNull DateTimeWithZone arrivalTimeAtDestination) {
+        return new Builder(
+                requireNonNull(remainingDistance),
+                remainingTimeSeconds,
+                requireNonNull(arrivalTimeAtDestination));
+    }
+
+    /**
+     * Constructs a new builder of {@link TravelEstimate}.
+     *
+     * @param remainingDistance        The estimated remaining {@link Distance} until arriving at
+     *                                 the destination.
+     * @param remainingTime            The estimated time remaining until arriving at the
+     *                                 destination.
+     * @param arrivalTimeAtDestination The arrival time with the time zone information provided for
+     *                                 the destination.
+     * @throws IllegalArgumentException if {@code remainingTime} contains a negative duration.
+     * @throws NullPointerException     if {@code remainingDistance} is {@code null}
+     * @throws NullPointerException     if {@code remainingTime} is {@code null}
+     * @throws NullPointerException     if {@code arrivalTimeAtDestination} is {@code null}
+     */
+    @NonNull
+    @RequiresApi(26)
+    @SuppressWarnings("AndroidJdkLibsChecker")
+    public static Builder builder(
+            @NonNull Distance remainingDistance,
+            @NonNull Duration remainingTime,
+            @NonNull ZonedDateTime arrivalTimeAtDestination) {
+        return new Builder(
+                requireNonNull(remainingDistance),
+                requireNonNull(remainingTime),
+                requireNonNull(arrivalTimeAtDestination));
+    }
+
+    @NonNull
+    public Distance getRemainingDistance() {
+        return requireNonNull(mRemainingDistance);
+    }
+
+    // TODO(rampara): Returned time values must be in milliseconds
+    @SuppressWarnings("MethodNameUnits")
+    public long getRemainingTimeSeconds() {
+        return mRemainingTimeSeconds;
+    }
+
+    @Nullable
+    public DateTimeWithZone getArrivalTimeAtDestination() {
+        return mArrivalTimeAtDestination;
+    }
+
+    @NonNull
+    public CarColor getRemainingTimeColor() {
+        return mRemainingTimeColor;
+    }
+
+    @NonNull
+    public CarColor getRemainingDistanceColor() {
+        return mRemainingDistanceColor;
+    }
+
+    @SuppressLint("UnsafeNewApiCall")
+    // TODO(rampara): Move API 26 calls into separate class.
+    @Override
+    @NonNull
+    @RequiresApi(26)
+    @SuppressWarnings("AndroidJdkLibsChecker")
+    public String toString() {
+        return "[ remaining distance: "
+                + mRemainingDistance
+                + ", time: "
+                + Duration.ofSeconds(mRemainingTimeSeconds)
+                + ", ETA: "
+                + mArrivalTimeAtDestination
+                + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mRemainingDistance,
+                mRemainingTimeSeconds,
+                mArrivalTimeAtDestination,
+                mRemainingTimeColor,
+                mRemainingDistanceColor);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof TravelEstimate)) {
+            return false;
+        }
+        TravelEstimate otherInfo = (TravelEstimate) other;
+
+        return Objects.equals(mRemainingDistance, otherInfo.mRemainingDistance)
+                && mRemainingTimeSeconds == otherInfo.mRemainingTimeSeconds
+                && Objects.equals(mArrivalTimeAtDestination, otherInfo.mArrivalTimeAtDestination)
+                && Objects.equals(mRemainingTimeColor, otherInfo.mRemainingTimeColor)
+                && Objects.equals(mRemainingDistanceColor, otherInfo.mRemainingDistanceColor);
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private TravelEstimate() {
+        mRemainingDistance = null;
+        mRemainingTimeSeconds = 0;
+        mArrivalTimeAtDestination = null;
+        mRemainingTimeColor = CarColor.DEFAULT;
+        mRemainingDistanceColor = CarColor.DEFAULT;
+    }
+
+    private TravelEstimate(Builder builder) {
+        this.mRemainingDistance = builder.mRemainingDistance;
+        this.mRemainingTimeSeconds = builder.mRemainingTimeSeconds;
+        this.mArrivalTimeAtDestination = builder.mArrivalTimeAtDestination;
+        this.mRemainingTimeColor = builder.mRemainingTimeColor;
+        this.mRemainingDistanceColor = builder.mRemainingDistanceColor;
+    }
+
+    /** A builder of {@link TravelEstimate}. */
+    public static final class Builder {
+        private final Distance mRemainingDistance;
+        private final long mRemainingTimeSeconds;
+        private final DateTimeWithZone mArrivalTimeAtDestination;
+        private CarColor mRemainingTimeColor = CarColor.DEFAULT;
+        private CarColor mRemainingDistanceColor = CarColor.DEFAULT;
+
+        private Builder(
+                Distance remainingDistance,
+                long remainingTimeSeconds,
+                DateTimeWithZone arrivalTimeAtDestination) {
+            this.mRemainingDistance = requireNonNull(remainingDistance);
+            this.mRemainingTimeSeconds = validateRemainingTime(remainingTimeSeconds);
+            this.mArrivalTimeAtDestination = requireNonNull(arrivalTimeAtDestination);
+        }
+
+        @SuppressLint("UnsafeNewApiCall")
+        // TODO(rampara): Move API 26 calls into separate class.
+        @RequiresApi(26)
+        @SuppressWarnings("AndroidJdkLibsChecker")
+        private Builder(
+                Distance remainingDistance,
+                Duration remainingTime,
+                ZonedDateTime arrivalTimeAtDestination) {
+            this.mRemainingDistance = remainingDistance;
+            this.mRemainingTimeSeconds = validateRemainingTime(remainingTime.getSeconds());
+            this.mArrivalTimeAtDestination = DateTimeWithZone.create(arrivalTimeAtDestination);
+        }
+
+        /**
+         * Sets the color of the remaining time text.
+         *
+         * <p>The host may ignore this color depending on the capabilities of the target screen.
+         *
+         * <p>If not set, {@link CarColor#DEFAULT} will be used.
+         *
+         * <p>Custom colors created with {@link CarColor#createCustom} are not supported.
+         *
+         * @throws IllegalArgumentException if {@code remainingTimeColor} is not supported.
+         * @throws NullPointerException     if {@code remainingTimecolor} is {@code null}
+         */
+        @NonNull
+        public Builder setRemainingTimeColor(@NonNull CarColor remainingTimeColor) {
+            CarColorConstraints.STANDARD_ONLY.validateOrThrow(requireNonNull(remainingTimeColor));
+            this.mRemainingTimeColor = remainingTimeColor;
+            return this;
+        }
+
+        /**
+         * Sets the color of the remaining distance text.
+         *
+         * <p>The host may ignore this color depending on the capabilities of the target screen.
+         *
+         * <p>If not set, {@link CarColor#DEFAULT} will be used.
+         *
+         * <p>Custom colors created with {@link CarColor#createCustom} are not supported.
+         *
+         * @throws IllegalArgumentException if {@code remainingDistanceColor} is not supported.
+         * @throws NullPointerException     if {@code remainingDistanceColor} is {@code null}.
+         */
+        @NonNull
+        public Builder setRemainingDistanceColor(@NonNull CarColor remainingDistanceColor) {
+            CarColorConstraints.STANDARD_ONLY.validateOrThrow(
+                    requireNonNull(remainingDistanceColor));
+            this.mRemainingDistanceColor = remainingDistanceColor;
+            return this;
+        }
+
+        /** Constructs the {@link TravelEstimate} defined by this builder. */
+        @NonNull
+        public TravelEstimate build() {
+            return new TravelEstimate(this);
+        }
+
+        private static long validateRemainingTime(long remainingTimeSeconds) {
+            if (remainingTimeSeconds < 0) {
+                throw new IllegalArgumentException(
+                        "Remaining time must be a larger than or equal to zero");
+            }
+            return remainingTimeSeconds;
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java
new file mode 100644
index 0000000..d3f7b5d
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java
@@ -0,0 +1,297 @@
+/*
+ * 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.car.app.navigation.model;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarText;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents information about a trip including destinations, steps, and travel estimates.
+ *
+ * <p>This information data <b>may</b> be displayed in different places in the car such as the
+ * instrument cluster screens and heads-up display.
+ */
+public final class Trip {
+    @Keep
+    private final List<Destination> mDestinations;
+    @Keep
+    private final List<Step> mSteps;
+    @Keep
+    private final List<TravelEstimate> mDestinationTravelEstimates;
+    @Keep
+    private final List<TravelEstimate> mStepTravelEstimates;
+    @Keep
+    @Nullable
+    private final CarText mCurrentRoad;
+    @Keep
+    private final boolean mIsLoading;
+
+    /** Constructs a new builder of {@link Trip}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @NonNull
+    public List<Destination> getDestinations() {
+        return mDestinations;
+    }
+
+    @NonNull
+    public List<Step> getSteps() {
+        return mSteps;
+    }
+
+    @NonNull
+    public List<TravelEstimate> getDestinationTravelEstimates() {
+        return mDestinationTravelEstimates;
+    }
+
+    @NonNull
+    public List<TravelEstimate> getStepTravelEstimates() {
+        return mStepTravelEstimates;
+    }
+
+    @Nullable
+    public CarText getCurrentRoad() {
+        return mCurrentRoad;
+    }
+
+    public boolean isLoading() {
+        return mIsLoading;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "[ destinations : "
+                + mDestinations.toString()
+                + ", steps: "
+                + mSteps.toString()
+                + ", dest estimates: "
+                + mDestinationTravelEstimates.toString()
+                + ", step estimates: "
+                + mStepTravelEstimates.toString()
+                + ", road: "
+                + CarText.toShortString(mCurrentRoad)
+                + ", isLoading: "
+                + mIsLoading
+                + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mDestinations, mSteps, mDestinationTravelEstimates, mStepTravelEstimates,
+                mCurrentRoad);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof Trip)) {
+            return false;
+        }
+
+        Trip otherTrip = (Trip) other;
+        return Objects.equals(mDestinations, otherTrip.mDestinations)
+                && Objects.equals(mSteps, otherTrip.mSteps)
+                && Objects.equals(
+                mDestinationTravelEstimates, otherTrip.mDestinationTravelEstimates)
+                && Objects.equals(mStepTravelEstimates, otherTrip.mStepTravelEstimates)
+                && Objects.equals(mCurrentRoad, otherTrip.mCurrentRoad)
+                && Objects.equals(mIsLoading, otherTrip.mIsLoading);
+    }
+
+    private Trip(Builder builder) {
+        this.mDestinations = new ArrayList<>(builder.mDestinations);
+        this.mSteps = new ArrayList<>(builder.mSteps);
+        this.mDestinationTravelEstimates = new ArrayList<>(builder.mDestinationTravelEstimates);
+        this.mStepTravelEstimates = new ArrayList<>(builder.mStepTravelEstimates);
+        this.mCurrentRoad = builder.mCurrentRoad;
+        this.mIsLoading = builder.mIsLoading;
+    }
+
+    /** Constructs an empty instance, used by serialization code. */
+    private Trip() {
+        mDestinations = Collections.emptyList();
+        mSteps = Collections.emptyList();
+        mDestinationTravelEstimates = Collections.emptyList();
+        mStepTravelEstimates = Collections.emptyList();
+        mCurrentRoad = null;
+        mIsLoading = false;
+    }
+
+    /** A builder of {@link Trip}. */
+    public static final class Builder {
+        private final List<Destination> mDestinations = new ArrayList<>();
+        private final List<Step> mSteps = new ArrayList<>();
+        private final List<TravelEstimate> mDestinationTravelEstimates = new ArrayList<>();
+        private final List<TravelEstimate> mStepTravelEstimates = new ArrayList<>();
+        @Nullable
+        private CarText mCurrentRoad;
+        private boolean mIsLoading;
+
+        /**
+         * Adds a destination to the trip.
+         *
+         * <p>Destinations must be added in order of arrival. A destination is not required. Display
+         * surfaces may or may not use the destination and if multiple destinations are added the
+         * display may only show information about the first destination.
+         *
+         * <p>For every destination added, a corresponding {@link TravelEstimate} must be added via
+         * {@link #addDestinationTravelEstimate}.They are added separately so that travel
+         * estimates can be updated frequently based on location.
+         */
+        @NonNull
+        public Builder addDestination(@NonNull Destination destination) {
+            mDestinations.add(requireNonNull(destination));
+            return this;
+        }
+
+        /** Clears the list of destinations in the builder. */
+        @NonNull
+        public Builder clearDestinations() {
+            mDestinations.clear();
+            return this;
+        }
+
+        /**
+         * Adds a step to the trip.
+         *
+         * <p>Steps must be added in order of arrival. A step is not required. Display surfaces
+         * may or may not use the step and if multiple steps are added the display may only show
+         * information about the first step.
+         *
+         * <p>For every step added, a corresponding {@link TravelEstimate} must be added via {@link
+         * #addStepTravelEstimate}. They are added separately so that travel estimates can be
+         * updated frequently based on location.
+         */
+        @NonNull
+        public Builder addStep(@Nullable Step step) {
+            mSteps.add(requireNonNull(step));
+            return this;
+        }
+
+        /** Clears the list of steps in the builder. */
+        @NonNull
+        public Builder clearSteps() {
+            mSteps.clear();
+            return this;
+        }
+
+        /**
+         * Adds a destination travel estimate to the trip.
+         *
+         * <p>Destination travel estimates must be added in order of arrival. A destination travel
+         * estimate is not required. Display surfaces may or may not use the destination travel
+         * estimate and if multiple destination travel estimates are added the display may only show
+         * information about the first destination travel estimate.
+         *
+         * <p>For every destination travel estimate added, a corresponding destination must also be
+         * added. They are added separately so that travel estimates can be updated frequently
+         * based on location.
+         */
+        @NonNull
+        public Builder addDestinationTravelEstimate(
+                @NonNull TravelEstimate destinationTravelEstimate) {
+            mDestinationTravelEstimates.add(requireNonNull(destinationTravelEstimate));
+            return this;
+        }
+
+        /** Clears the list of destination travel estimates in the builder. */
+        @NonNull
+        public Builder clearDestinationTravelEstimates() {
+            mDestinationTravelEstimates.clear();
+            return this;
+        }
+
+        /**
+         * Adds a step travel estimate to the trip.
+         *
+         * <p>Step travel estimates must be added in order of arrival. A step travel estimate is not
+         * required. Display surfaces may or may not use the step travel estimate and if multiple
+         * step travel estimates are added the display may only show information about the first
+         * step travel estimate.
+         *
+         * <p>For every step travel estimate added, a corresponding step must also be added.
+         */
+        @NonNull
+        public Builder addStepTravelEstimate(@NonNull TravelEstimate stepTravelEstimate) {
+            mStepTravelEstimates.add(requireNonNull(stepTravelEstimate));
+            return this;
+        }
+
+        /** Clears the list of destination travel estimates in the builder. */
+        @NonNull
+        public Builder clearStepTravelEstimates() {
+            mStepTravelEstimates.clear();
+            return this;
+        }
+
+        /** Sets a text description of the current road or {@code null} if unknown. */
+        @NonNull
+        public Builder setCurrentRoad(@Nullable CharSequence currentRoad) {
+            this.mCurrentRoad = currentRoad == null ? null : CarText.create(currentRoad);
+            return this;
+        }
+
+        /**
+         * Sets whether the {@link Trip} is in a loading state.
+         *
+         * <p>If set to {@code true}, the UI may show a loading indicator, and adding any steps
+         * or step travel estimates will throw an {@link IllegalArgumentException}.
+         */
+        // TODO(rampara): Consider renaming to setLoading()
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setIsLoading(boolean isLoading) {
+            this.mIsLoading = isLoading;
+            return this;
+        }
+
+        /**
+         * Constructs the {@link Trip} defined by this builder.
+         */
+        @NonNull
+        public Trip build() {
+            if (mDestinations.size() != mDestinationTravelEstimates.size()) {
+                throw new IllegalArgumentException(
+                        "Destinations and destination travel estimates sizes must match");
+            }
+            if (mSteps.size() != mStepTravelEstimates.size()) {
+                throw new IllegalArgumentException(
+                        "Steps and step travel estimates sizes must match");
+            }
+            if (mIsLoading && !mSteps.isEmpty()) {
+                throw new IllegalArgumentException("Step information may not be set while loading");
+            }
+            return new Trip(this);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java b/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java
new file mode 100644
index 0000000..05fc5a1
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java
@@ -0,0 +1,529 @@
+/*
+ * 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.car.app.notification;
+
+import static java.util.Objects.requireNonNull;
+
+import android.app.Notification;
+import android.app.Notification.Action;
+import android.app.PendingIntent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to add car app extensions to notifications.
+ *
+ * <p>By default, notifications in a car screen have the properties provided by
+ * {@link NotificationCompat.Builder}. This helper class provides methods to
+ * override those properties for the car screen. However, notifications only show up in the car
+ * screen if it is extended with {@link CarAppExtender}, even if the extender does not override any
+ * properties. To create a notification with car extensions:
+ *
+ * <ol>
+ *   <li>Create a {@link NotificationCompat.Builder}, setting any desired properties.
+ *   <li>Create a {@link CarAppExtender.Builder} with {@link CarAppExtender#builder()}.
+ *   <li>Set car-specific properties using the {@code set} methods of {@link
+ *       CarAppExtender.Builder}.
+ *   <li>Create a {@link CarAppExtender} by calling {@link Builder#build()}.
+ *   <li>Call {@link NotificationCompat.Builder#extend} to apply the extensions to a notification.
+ *   <li>Post the notification to the notification system with the {@code
+ *       NotificationManagerCompat.notify(...)} methods and not the {@code
+ *       NotificationManager.notify(...)} methods.
+ * </ol>
+ *
+ * <pre class="prettyprint">
+ * Notification notification = new NotificationCompat.Builder(context)
+ *         ...
+ *         .extend(CarAppExtender.builder()
+ *                 .set*(...)
+ *                 .build())
+ *         .build();
+ * </pre>
+ *
+ * <p>Car extensions can be accessed on an existing notification by using the {@code
+ * CarAppExtender(Notification)} constructor, and then using the {@code get} methods to access
+ * values.
+ *
+ * <p>The car screen UI is affected by the notification channel importance (Android O and above) or
+ * notification priority (below Android O) in the following ways:
+ *
+ * <ul>
+ *   <li>A heads-up-notification (HUN) will show if the importance is set to
+ *   {@link NotificationManagerCompat#IMPORTANCE_HIGH}, or the priority is set
+ *       to {@link NotificationCompat#PRIORITY_HIGH} or above.
+ *   <li>The notification center icon, which opens a screen with all posted notifications when
+ *       tapped, will show a badge for a new notification if the importance is set to
+ *       {@link NotificationManagerCompat#IMPORTANCE_DEFAULT} or above, or the
+ *       priority is set to {@link NotificationCompat#PRIORITY_DEFAULT} or above.
+ *   <li>The notification entry will show in the notification center for all priority levels.
+ * </ul>
+ *
+ * Calling {@link Builder#setImportance(int)} will override the importance for the notification in
+ * the car screen.
+ *
+ * <p>Calling {@code NotificationCompat.Builder#setOnlyAlertOnce(true)} will alert a high-priority
+ * notification only once in the HUN. Updating the same notification will not trigger another HUN
+ * event.
+ *
+ * <h4>Navigation</h4>
+ *
+ * <p>For a navigation app's turn-by-turn (TBT) notifications, which update the same notification
+ * frequently with navigation information, the notification UI has a slightly different behavior.
+ * The app can post a TBT notification by calling {@code
+ * NotificationCompat.Builder#setOngoing(true)} and {@code
+ * NotificationCompat.Builder#setCategory(NotificationCompat.CATEGORY_NAVIGATION)}. The car screen
+ * UI is affected in the following ways:
+ *
+ * <ul>
+ *   <li>The same heads-up-notification (HUN) behavior as regular notifications.
+ *   <li>A rail widget at the bottom of the screen will show when the navigation app is in the
+ *       background.
+ * </ul>
+ *
+ * Note that frequent HUNs distract the driver. The recommended practice is to update the TBT
+ * notification regularly on distance changes, which updates the rail widget, but call {@code
+ * NotificationCompat.Builder#setOnlyAlertOnce(true)} unless there is a significant navigation turn
+ * event.
+ */
+public class CarAppExtender implements NotificationCompat.Extender {
+    private static final String EXTRA_CAR_EXTENDER = "android.car.EXTENSIONS";
+    private static final String EXTRA_IS_EXTENDED = "android.car.app.EXTENDED";
+    private static final String EXTRA_CONTENT_TITLE = "content_title";
+    private static final String EXTRA_CONTENT_TEXT = "content_text";
+    private static final String EXTRA_SMALL_RES_ID = "small_res_id";
+    private static final String EXTRA_LARGE_BITMAP = "large_bitmap";
+    private static final String EXTRA_CONTENT_INTENT = "content_intent";
+    private static final String EXTRA_DELETE_INTENT = "delete_intent";
+    private static final String EXTRA_ACTIONS = "actions";
+    private static final String EXTRA_IMPORTANCE = "importance";
+
+    private boolean mIsExtended;
+    @Nullable
+    private CharSequence mContentTitle;
+    @Nullable
+    private CharSequence mContentText;
+    private int mSmallIconResId;
+    @Nullable
+    private Bitmap mLargeIconBitmap;
+    @Nullable
+    private PendingIntent mContentIntent;
+    @Nullable
+    private PendingIntent mDeleteIntent;
+    private ArrayList<Action> mActions;
+    private int mImportance;
+
+    /** Creates a {@link CarAppExtender.Builder}. */
+    @NonNull
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Creates a {@link CarAppExtender} from the {@link CarAppExtender} of an existing notification.
+     */
+    public CarAppExtender(@NonNull Notification notification) {
+        Bundle extras = NotificationCompat.getExtras(notification);
+        if (extras == null) {
+            return;
+        }
+
+        Bundle carBundle = extras.getBundle(EXTRA_CAR_EXTENDER);
+        if (carBundle == null) {
+            return;
+        }
+
+        mIsExtended = carBundle.getBoolean(EXTRA_IS_EXTENDED);
+        mContentTitle = carBundle.getCharSequence(EXTRA_CONTENT_TITLE);
+        mContentText = carBundle.getCharSequence(EXTRA_CONTENT_TEXT);
+        mSmallIconResId = carBundle.getInt(EXTRA_SMALL_RES_ID);
+        mLargeIconBitmap = carBundle.getParcelable(EXTRA_LARGE_BITMAP);
+        mContentIntent = carBundle.getParcelable(EXTRA_CONTENT_INTENT);
+        mDeleteIntent = carBundle.getParcelable(EXTRA_DELETE_INTENT);
+        ArrayList<Action> actions = carBundle.getParcelableArrayList(EXTRA_ACTIONS);
+        this.mActions = actions == null ? new ArrayList<>() : actions;
+        mImportance =
+                carBundle.getInt(EXTRA_IMPORTANCE,
+                        NotificationManagerCompat.IMPORTANCE_UNSPECIFIED);
+    }
+
+    private CarAppExtender(Builder builder) {
+        this.mContentTitle = builder.mContentTitle;
+        this.mContentText = builder.mContentText;
+        this.mSmallIconResId = builder.mSmallIconResId;
+        this.mLargeIconBitmap = builder.mLargeIconBitmap;
+        this.mContentIntent = builder.mContentIntent;
+        this.mDeleteIntent = builder.mDeleteIntent;
+        this.mActions = builder.mActions;
+        this.mImportance = builder.mImportance;
+    }
+
+    /**
+     * Applies car extensions to a notification that is being built. This is typically called by
+     * {@link NotificationCompat.Builder#extend(NotificationCompat.Extender)}.
+     *
+     * @throws NullPointerException if {@code builder} is {@code null}.
+     */
+    @NonNull
+    @Override
+    public NotificationCompat.Builder extend(@NonNull NotificationCompat.Builder builder) {
+        requireNonNull(builder);
+        Bundle carExtensions = new Bundle();
+
+        carExtensions.putBoolean(EXTRA_IS_EXTENDED, true);
+
+        if (mContentTitle != null) {
+            carExtensions.putCharSequence(EXTRA_CONTENT_TITLE, mContentTitle);
+        }
+
+        if (mContentText != null) {
+            carExtensions.putCharSequence(EXTRA_CONTENT_TEXT, mContentText);
+        }
+
+        if (mSmallIconResId != Resources.ID_NULL) {
+            carExtensions.putInt(EXTRA_SMALL_RES_ID, mSmallIconResId);
+        }
+
+        if (mLargeIconBitmap != null) {
+            carExtensions.putParcelable(EXTRA_LARGE_BITMAP, mLargeIconBitmap);
+        }
+
+        if (mContentIntent != null) {
+            carExtensions.putParcelable(EXTRA_CONTENT_INTENT, mContentIntent);
+        }
+
+        if (mDeleteIntent != null) {
+            carExtensions.putParcelable(EXTRA_DELETE_INTENT, mDeleteIntent);
+        }
+
+        if (!mActions.isEmpty()) {
+            carExtensions.putParcelableArrayList(EXTRA_ACTIONS, mActions);
+        }
+
+        carExtensions.putInt(EXTRA_IMPORTANCE, mImportance);
+
+        builder.getExtras().putBundle(EXTRA_CAR_EXTENDER, carExtensions);
+        return builder;
+    }
+
+    /**
+     * Returns {@code true} if the notification was extended with {@link CarAppExtender}, {@code
+     * false} otherwise.
+     */
+    public boolean isExtended() {
+        return mIsExtended;
+    }
+
+    /**
+     * Returns {@code true} if the notification was extended with {@link CarAppExtender}, {@code
+     * false} otherwise.
+     *
+     * @throws NullPointerException if {@code notification} is {@code null}.
+     */
+    public static boolean isExtended(@NonNull Notification notification) {
+        Bundle extras = NotificationCompat.getExtras(requireNonNull(notification));
+        if (extras == null) {
+            return false;
+        }
+
+        extras = extras.getBundle(EXTRA_CAR_EXTENDER);
+        return extras != null && extras.getBoolean(EXTRA_IS_EXTENDED);
+    }
+
+    /**
+     * Gets the content title for the notification.
+     *
+     * @see Builder#setContentTitle
+     */
+    @Nullable
+    public CharSequence getContentTitle() {
+        return mContentTitle;
+    }
+
+    /**
+     * Returns the content text of the notification.
+     *
+     * @see Builder#setContentText
+     */
+    @Nullable
+    public CharSequence getContentText() {
+        return mContentText;
+    }
+
+    /**
+     * Gets the resource ID of the small icon drawable to use.
+     *
+     * @see Builder#setSmallIcon(int)
+     */
+    public int getSmallIconResId() {
+        return mSmallIconResId;
+    }
+
+    /**
+     * Gets the large icon bitmap to display in the notification.
+     *
+     * @see Builder#setLargeIcon(Bitmap)
+     */
+    @Nullable
+    public Bitmap getLargeIconBitmap() {
+        return mLargeIconBitmap;
+    }
+
+    /**
+     * Gets the {@link PendingIntent} to send when the notification is clicked in the car, or {@code
+     * null} if one is not set.
+     *
+     * @see Builder#setContentIntent(PendingIntent)
+     */
+    @Nullable
+    public PendingIntent getContentIntent() {
+        return mContentIntent;
+    }
+
+    /**
+     * Gets the {@link PendingIntent} to send when the notification is cleared by the user, or
+     * {@code null} if one is not set.
+     *
+     * @see Builder#setDeleteIntent(PendingIntent)
+     */
+    @Nullable
+    public PendingIntent getDeleteIntent() {
+        return mDeleteIntent;
+    }
+
+    /**
+     * Gets the list of {@link Action} present on this car notification.
+     *
+     * @see Builder#addAction(int, CharSequence, PendingIntent)
+     */
+    @NonNull
+    public List<Action> getActions() {
+        return mActions;
+    }
+
+    /**
+     * Gets the importance of the notification in the car screen.
+     *
+     * @see Builder#setImportance(int)
+     */
+    public int getImportance() {
+        return mImportance;
+    }
+
+    /** A builder of {@link CarAppExtender}. */
+    public static final class Builder {
+        @Nullable
+        private CharSequence mContentTitle;
+        @Nullable
+        private CharSequence mContentText;
+        private int mSmallIconResId;
+        @Nullable
+        private Bitmap mLargeIconBitmap;
+        @Nullable
+        private PendingIntent mContentIntent;
+        @Nullable
+        private PendingIntent mDeleteIntent;
+        private final ArrayList<Action> mActions = new ArrayList<>();
+        private int mImportance = NotificationManagerCompat.IMPORTANCE_UNSPECIFIED;
+
+        /**
+         * Sets the title of the notification in the car screen, or {@code null} to not override the
+         * notification title.
+         *
+         * <p>This will be the most prominently displayed text in the car notification.
+         *
+         * <p>This method is equivalent to
+         * {@link NotificationCompat.Builder#setContentTitle(CharSequence)} for the car
+         * screen.
+         */
+        @NonNull
+        public Builder setContentTitle(@Nullable CharSequence contentTitle) {
+            this.mContentTitle = contentTitle;
+            return this;
+        }
+
+        /**
+         * Sets the content text of the notification in the car screen, or {@code null} to not
+         * override the content text.
+         *
+         * <p>This method is equivalent to
+         * {@link NotificationCompat.Builder#setContentText(CharSequence)} for the car screen.
+         *
+         * @param contentText override for the notification's content text. If set to an empty
+         *                    string, it will be treated as if there is no context text.
+         * @throws NullPointerException if {@code contentText} is {@code null}
+         */
+        @NonNull
+        public Builder setContentText(@Nullable CharSequence contentText) {
+            this.mContentText = contentText;
+            return this;
+        }
+
+        /**
+         * Sets the small icon of the notification in the car screen, or
+         * {@link Resources#ID_NULL} to not override the notification small icon.
+         *
+         * <p>This is used as the primary icon to represent the notification.
+         *
+         * <p>This method is equivalent to {@link NotificationCompat.Builder#setSmallIcon(int)} for
+         * the car screen.
+         */
+        // TODO(rampara): Revisit small icon getter API
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setSmallIcon(int iconResId) {
+            this.mSmallIconResId = iconResId;
+            return this;
+        }
+
+        /**
+         * Sets the large icon of the notification in the car screen, or {@code null} to not
+         * override the large icon of the notification.
+         *
+         * <p>This is used as the secondary icon to represent the notification in the notification
+         * center.
+         *
+         * <p>This method is equivalent to {@link NotificationCompat.Builder#setLargeIcon(Bitmap)}
+         * for the car screen.
+         *
+         * <p>The large icon will be shown in the notification badge. If the large icon is not
+         * set in the {@link CarAppExtender} or the notification, the small icon will show instead.
+         */
+        // TODO(rampara): Revisit small icon getter API
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setLargeIcon(@Nullable Bitmap bitmap) {
+            this.mLargeIconBitmap = bitmap;
+            return this;
+        }
+
+        /**
+         * Supplies a {@link PendingIntent} to send when the notification is clicked in the car.
+         *
+         * <p>If set to {@code null}, the notification's content intent will be used.
+         *
+         * <p>In the case of navigation notifications in the rail widget, this intent will be
+         * sent when the user taps on the rail widget.
+         *
+         * <p>This method is equivalent to
+         * {@link NotificationCompat.Builder#setContentIntent(PendingIntent)} for the car screen.
+         *
+         * @param contentIntent override for the notification's content intent.
+         */
+        @NonNull
+        public Builder setContentIntent(@Nullable PendingIntent contentIntent) {
+            this.mContentIntent = contentIntent;
+            return this;
+        }
+
+        /**
+         * Supplies a {@link PendingIntent} to send when the user clears the notification by either
+         * using the "clear all" functionality in the notification center, or tapping the individual
+         * "close" buttons on the notification in the car screen.
+         *
+         * <p>If set to {@code null}, the notification's content intent will be used.
+         *
+         * <p>This method is equivalent to
+         * {@link NotificationCompat.Builder#setDeleteIntent(PendingIntent)} for the car screen.
+         *
+         * @param deleteIntent override for the notification's delete intent.
+         */
+        @NonNull
+        public Builder setDeleteIntent(@Nullable PendingIntent deleteIntent) {
+            this.mDeleteIntent = deleteIntent;
+            return this;
+        }
+
+        /**
+         * Adds an action to this notification.
+         *
+         * <p>Actions are typically displayed by the system as a button adjacent to the notification
+         * content.
+         *
+         * <p>A notification may offer up to 2 actions. The system may not display some actions
+         * in the compact notification UI (e.g. heads-up-notifications).
+         *
+         * <p>If one or more action is added with this method, any action added by
+         * {@link NotificationCompat.Builder#addAction(int, CharSequence, PendingIntent)} will be
+         * ignored.
+         *
+         * <p>This method is equivalent to
+         * {@link NotificationCompat.Builder#addAction(int, CharSequence, PendingIntent)} for the
+         * car screen.
+         *
+         * @param icon   resource ID of a drawable that represents the action. In order to
+         *               display the
+         *               actions properly, a valid resource id for the icon must be provided.
+         * @param title  text describing the action.
+         * @param intent {@link PendingIntent} to send when the action is invoked. In the case of
+         *               navigation notifications in the rail widget, this intent will be sent
+         *               when the user taps on the action icon in the rail
+         *               widget.
+         * @throws NullPointerException if {@code title} is {@code null}.
+         * @throws NullPointerException if {@code intent} is {@code null}.
+         */
+        // TODO(rampara): Update to remove use of deprecated Action API
+        @SuppressWarnings("deprecation")
+        @NonNull
+        public Builder addAction(
+                @DrawableRes int icon, @NonNull CharSequence title, @NonNull PendingIntent intent) {
+            this.mActions.add(new Action(icon, requireNonNull(title), requireNonNull(intent)));
+            return this;
+        }
+
+        /** Clears any actions that may have been added with {@link #addAction} up to this point. */
+        @NonNull
+        public Builder clearActions() {
+            this.mActions.clear();
+            return this;
+        }
+
+        /**
+         * Sets the importance of the notification in the car screen.
+         *
+         * <p>The default value is {@link NotificationManagerCompat#IMPORTANCE_UNSPECIFIED}.
+         *
+         * <p>The importance is used to determine whether the notification will show as a HUN on
+         * the car screen. See the class description for more details.
+         *
+         * <p>See {@link NotificationManagerCompat} for all supported importance
+         * values.
+         */
+        @NonNull
+        public Builder setImportance(int importance) {
+            this.mImportance = importance;
+            return this;
+        }
+
+        /**
+         * Constructs the {@link CarAppExtender} defined by this builder.
+         */
+        @NonNull
+        public CarAppExtender build() {
+            return new CarAppExtender(this);
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/serialization/Bundleable.java b/car/app/app/src/main/java/androidx/car/app/serialization/Bundleable.java
index e8b3579..c25dec2 100644
--- a/car/app/app/src/main/java/androidx/car/app/serialization/Bundleable.java
+++ b/car/app/app/src/main/java/androidx/car/app/serialization/Bundleable.java
@@ -75,7 +75,10 @@
             new Creator<Bundleable>() {
                 @Override
                 public Bundleable createFromParcel(final Parcel source) {
-                    return new Bundleable(requireNonNull(source.readBundle()));
+                    // To work around a lint warning that indicates the default class loader will
+                    // not work for restoring our own classes.
+                    return new Bundleable(
+                            requireNonNull(source.readBundle(getClass().getClassLoader())));
                 }
 
                 @Override
diff --git a/car/app/app/src/main/java/androidx/car/app/serialization/Bundler.java b/car/app/app/src/main/java/androidx/car/app/serialization/Bundler.java
index 26c5a04..7f325a0 100644
--- a/car/app/app/src/main/java/androidx/car/app/serialization/Bundler.java
+++ b/car/app/app/src/main/java/androidx/car/app/serialization/Bundler.java
@@ -808,8 +808,9 @@
         }
     }
 
-    /** A decorator on a {@link BundlerException} that tacks the frame information on its message
-     * . */
+    /**
+     * A decorator on a {@link BundlerException} that tacks the frame information on its message.
+     */
     static class TracedBundlerException extends BundlerException {
         TracedBundlerException(String msg, Trace trace) {
             super(msg + ", frames: " + trace.toFlatString());
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/Logger.java b/car/app/app/src/main/java/androidx/car/app/utils/Logger.java
index 0a8cd1f..a402482 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/Logger.java
+++ b/car/app/app/src/main/java/androidx/car/app/utils/Logger.java
@@ -16,17 +16,11 @@
 
 package androidx.car.app.utils;
 
-import static androidx.annotation.RestrictTo.Scope.LIBRARY;
-
 import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
 
 /**
  * Logger interface to allow the host to log while using the client library.
- *
- * @hide
  */
-@RestrictTo(LIBRARY)
 // TODO: Allow setting logging severity and including throwables
 public interface Logger {
     void log(@NonNull String message);
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java b/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
index 85d408d..1b2ed7e 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
+++ b/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
@@ -155,7 +155,8 @@
     // TODO(rampara): Change method signature to change parameter order.
     @SuppressLint("LambdaLast")
     public static void dispatchHostCall(
-            @NonNull HostCall hostCall, @NonNull IOnDoneCallback callback, @NonNull String callName) {
+            @NonNull HostCall hostCall, @NonNull IOnDoneCallback callback,
+            @NonNull String callName) {
         ThreadUtils.runOnMain(
                 () -> {
                     try {
@@ -188,17 +189,15 @@
     public static void sendFailureResponse(@NonNull IOnDoneCallback callback,
             @NonNull String callName,
             @NonNull Throwable e) {
-        call(
-                () -> {
-                    try {
-                        callback.onFailure(Bundleable.create(new FailureResponse(e)));
-                    } catch (BundlerException bundlerException) {
-                        // Not possible, but catching since BundlerException is not runtime.
-                        throw new IllegalStateException(
-                                "Serialization failure in " + callName, bundlerException);
-                    }
-                    return null;
-                },
-                callName + " onFailure");
+        call(() -> {
+            try {
+                callback.onFailure(Bundleable.create(new FailureResponse(e)));
+            } catch (BundlerException bundlerException) {
+                // Not possible, but catching since BundlerException is not runtime.
+                throw new IllegalStateException(
+                        "Serialization failure in " + callName, bundlerException);
+            }
+            return null;
+        }, callName + " onFailure");
     }
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/ValidationUtils.java b/car/app/app/src/main/java/androidx/car/app/utils/ValidationUtils.java
index 1522107..3e4a283 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/ValidationUtils.java
+++ b/car/app/app/src/main/java/androidx/car/app/utils/ValidationUtils.java
@@ -19,15 +19,15 @@
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.GridItem;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Toggle;
 
 import java.util.List;
-
-// TODO(rampara): Uncomment on addition of model module
-//import androidx.car.app.model.CarText;
-//import androidx.car.app.model.GridItem;
-//import androidx.car.app.model.Row;
-//import androidx.car.app.model.Toggle;
+import java.util.Objects;
 
 /**
  * Shared util methods for handling different distraction validation logic.
@@ -80,22 +80,21 @@
                 return false;
             }
 
-            // TODO(rampara): Uncomment on addition of model module
-//            if (itemObj1 instanceof Row) {
-//                if (!rowsHaveSameContent((Row) itemObj1, (Row) itemObj2, i, logger)) {
-//                    return false;
-//                }
-//            } else if (itemObj1 instanceof GridItem) {
-//                if (!gridItemsHaveSameContent(
-//                        (GridItem) itemObj1,
-//                        itemList1SelectedIndex == i,
-//                        (GridItem) itemObj2,
-//                        itemList2SelectedIndex == i,
-//                        i,
-//                        logger)) {
-//                    return false;
-//                }
-//            }
+            if (itemObj1 instanceof Row) {
+                if (!rowsHaveSameContent((Row) itemObj1, (Row) itemObj2, i, logger)) {
+                    return false;
+                }
+            } else if (itemObj1 instanceof GridItem) {
+                if (!gridItemsHaveSameContent(
+                        (GridItem) itemObj1,
+                        itemList1SelectedIndex == i,
+                        (GridItem) itemObj2,
+                        itemList2SelectedIndex == i,
+                        i,
+                        logger)) {
+                    return false;
+                }
+            }
         }
 
         return true;
@@ -105,119 +104,116 @@
      * Returns {@code true} if the string contents of the two rows are equal, {@code false}
      * otherwise.
      */
-    // TODO(rampara): Uncomment on addition of model module
-//    private static boolean rowsHaveSameContent(Row row1, Row row2, int index, Logger logger) {
-//        // Special case for rows with toggles - if the toggle state has changed, then text updates
-//        // are allowed.
-//        if (rowToggleStateHasChanged(row1, row2)) {
-//            return true;
-//        }
-//
-//        if (!carTextsHasSameString(row1.getTitle(), row2.getTitle())) {
-//            logger.log(
-//                    "Different row titles at index "
-//                            + index
-//                            + ". Old: "
-//                            + row1.getTitle()
-//                            + ". New: "
-//                            + row2.getTitle());
-//            return false;
-//        }
-//
-//        List<CarText> row1Texts = row1.getText();
-//        List<CarText> row2Texts = row2.getText();
-//        if (row1Texts.size() != row2Texts.size()) {
-//            logger.log(
-//                    "Different text list size at row index "
-//                            + index
-//                            + ". Old: "
-//                            + row1Texts.size()
-//                            + ". New: "
-//                            + row2Texts.size());
-//            return false;
-//        }
-//
-//        for (int j = 0; j < row1Texts.size(); j++) {
-//            if (!carTextsHasSameString(row1Texts.get(j), row2Texts.get(j))) {
-//                logger.log(
-//                        "Different texts at row index "
-//                                + index
-//                                + ". Old row: "
-//                                + row1Texts.get(j)
-//                                + ". New row: "
-//                                + row2Texts.get(j));
-//                return false;
-//            }
-//        }
-//
-//        return true;
-//    }
+    private static boolean rowsHaveSameContent(Row row1, Row row2, int index, Logger logger) {
+        // Special case for rows with toggles - if the toggle state has changed, then text updates
+        // are allowed.
+        if (rowToggleStateHasChanged(row1, row2)) {
+            return true;
+        }
+
+        if (!carTextsHasSameString(row1.getTitle(), row2.getTitle())) {
+            logger.log(
+                    "Different row titles at index "
+                            + index
+                            + ". Old: "
+                            + row1.getTitle()
+                            + ". New: "
+                            + row2.getTitle());
+            return false;
+        }
+
+        List<CarText> row1Texts = row1.getTexts();
+        List<CarText> row2Texts = row2.getTexts();
+        if (row1Texts.size() != row2Texts.size()) {
+            logger.log(
+                    "Different text list size at row index "
+                            + index
+                            + ". Old: "
+                            + row1Texts.size()
+                            + ". New: "
+                            + row2Texts.size());
+            return false;
+        }
+
+        for (int j = 0; j < row1Texts.size(); j++) {
+            if (!carTextsHasSameString(row1Texts.get(j), row2Texts.get(j))) {
+                logger.log(
+                        "Different texts at row index "
+                                + index
+                                + ". Old row: "
+                                + row1Texts.get(j)
+                                + ". New row: "
+                                + row2Texts.get(j));
+                return false;
+            }
+        }
+
+        return true;
+    }
 
     /**
      * Returns {@code true} if string contents and images of the two grid items are equal, {@code
      * false} otherwise.
      */
-    // TODO(rampara): Uncomment on addition of model module
-//    private static boolean gridItemsHaveSameContent(
-//            GridItem gridItem1,
-//            boolean isGridItem1Selected,
-//            GridItem gridItem2,
-//            boolean isGridItem2Selected,
-//            int index,
-//            Logger logger) {
-//        // Special case for grid items with toggles - if the toggle state has changed, then text
-//        // and image updates are allowed.
-//        if (gridItemToggleStateHasChanged(gridItem1, gridItem2)) {
-//            return true;
-//        }
-//
-//        // Special case for grid items that are selectable - if the selected state has changed,
-//        then
-//        // text and image updates are allowed.
-//        if (isGridItem1Selected != isGridItem2Selected) {
-//            return true;
-//        }
-//
-//        if (!carTextsHasSameString(gridItem1.getTitle(), gridItem2.getTitle())) {
-//            logger.log(
-//                    "Different grid item titles at index "
-//                            + index
-//                            + ". Old: "
-//                            + gridItem1.getTitle()
-//                            + ". New: "
-//                            + gridItem2.getTitle());
-//            return false;
-//        }
-//
-//        if (!carTextsHasSameString(gridItem1.getText(), gridItem2.getText())) {
-//            logger.log(
-//                    "Different grid item texts at index "
-//                            + index
-//                            + ". Old: "
-//                            + gridItem1.getText()
-//                            + ". New: "
-//                            + gridItem2.getText());
-//            return false;
-//        }
-//
-//        if (!Objects.equals(gridItem1.getImage(), gridItem2.getImage())) {
-//            logger.log("Different grid item images at index " + index);
-//            return false;
-//        }
-//
-//        if (gridItem1.getImageType() != gridItem2.getImageType()) {
-//            logger.log(
-//                    "Different grid item image types at index "
-//                            + index
-//                            + ". Old: "
-//                            + gridItem1.getImageType()
-//                            + ". New: "
-//                            + gridItem2.getImageType());
-//            return false;
-//        }
-//
-//        return true;
-//    }
+    private static boolean gridItemsHaveSameContent(
+            GridItem gridItem1,
+            boolean isGridItem1Selected,
+            GridItem gridItem2,
+            boolean isGridItem2Selected,
+            int index,
+            Logger logger) {
+        // Special case for grid items with toggles - if the toggle state has changed, then text
+        // and image updates are allowed.
+        if (gridItemToggleStateHasChanged(gridItem1, gridItem2)) {
+            return true;
+        }
+
+        // Special case for grid items that are selectable - if the selected state has changed,
+        // then text and image updates are allowed.
+        if (isGridItem1Selected != isGridItem2Selected) {
+            return true;
+        }
+
+        if (!carTextsHasSameString(gridItem1.getTitle(), gridItem2.getTitle())) {
+            logger.log(
+                    "Different grid item titles at index "
+                            + index
+                            + ". Old: "
+                            + gridItem1.getTitle()
+                            + ". New: "
+                            + gridItem2.getTitle());
+            return false;
+        }
+
+        if (!carTextsHasSameString(gridItem1.getText(), gridItem2.getText())) {
+            logger.log(
+                    "Different grid item texts at index "
+                            + index
+                            + ". Old: "
+                            + gridItem1.getText()
+                            + ". New: "
+                            + gridItem2.getText());
+            return false;
+        }
+
+        if (!Objects.equals(gridItem1.getImage(), gridItem2.getImage())) {
+            logger.log("Different grid item images at index " + index);
+            return false;
+        }
+
+        if (gridItem1.getImageType() != gridItem2.getImageType()) {
+            logger.log(
+                    "Different grid item image types at index "
+                            + index
+                            + ". Old: "
+                            + gridItem1.getImageType()
+                            + ". New: "
+                            + gridItem2.getImageType());
+            return false;
+        }
+
+        return true;
+    }
 
     /**
      * Returns {@code true} if the strings of the two {@link CarText}s are the same, {@code false}
@@ -225,32 +221,31 @@
      *
      * <p>Spans that are attached to the strings are ignored from the comparison.
      */
-    // TODO(rampara): Uncomment on addition of model module
-//    private static boolean carTextsHasSameString(
-//            @Nullable CarText carText1, @Nullable CarText carText2) {
-//        // If both carText1 and carText2 are null, return true. If only one of them is null,
-//        return
-//        // false.
-//        if (carText1 == null || carText2 == null) {
-//            return carText1 == null && carText2 == null;
-//        }
-//
-//        return Objects.equals(carText1.getText(), carText2.getText());
-//    }
-//
-//    private static boolean rowToggleStateHasChanged(Row row1, Row row2) {
-//        Toggle toggle1 = row1.getToggle();
-//        Toggle toggle2 = row2.getToggle();
-//
-//        return toggle1 != null && toggle2 != null && toggle1.isChecked() != toggle2.isChecked();
-//    }
-//
-//    private static boolean gridItemToggleStateHasChanged(GridItem gridItem1, GridItem gridItem2) {
-//        Toggle toggle1 = gridItem1.getToggle();
-//        Toggle toggle2 = gridItem2.getToggle();
-//
-//        return toggle1 != null && toggle2 != null && toggle1.isChecked() != toggle2.isChecked();
-//    }
+    private static boolean carTextsHasSameString(
+            @Nullable CarText carText1, @Nullable CarText carText2) {
+        // If both carText1 and carText2 are null, return true. If only one of them is null,
+        // return false.
+        if (carText1 == null || carText2 == null) {
+            return carText1 == null && carText2 == null;
+        }
+
+        return Objects.equals(carText1.getText(), carText2.getText());
+    }
+
+    private static boolean rowToggleStateHasChanged(Row row1, Row row2) {
+        Toggle toggle1 = row1.getToggle();
+        Toggle toggle2 = row2.getToggle();
+
+        return toggle1 != null && toggle2 != null && toggle1.isChecked() != toggle2.isChecked();
+    }
+
+    private static boolean gridItemToggleStateHasChanged(GridItem gridItem1, GridItem gridItem2) {
+        Toggle toggle1 = gridItem1.getToggle();
+        Toggle toggle2 = gridItem2.getToggle();
+
+        return toggle1 != null && toggle2 != null && toggle1.isChecked() != toggle2.isChecked();
+    }
+
     private ValidationUtils() {
     }
 }
diff --git a/car/app/app/src/main/res/values/attrs.xml b/car/app/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..a481b69
--- /dev/null
+++ b/car/app/app/src/main/res/values/attrs.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<resources>
+    <attr name="carColorPrimary" format="color" />
+    <attr name="carColorPrimaryDark" format="color" />
+    <attr name="carColorSecondary" format="color" />
+    <attr name="carColorSecondaryDark" format="color" />
+</resources>
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PropKey.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PropKey.kt
index dd0eb34..7b55b2e 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PropKey.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PropKey.kt
@@ -41,7 +41,7 @@
  * [AnimationVector] type.
  *
  * @param convertToVector converts from type [T] to [AnimationVector]
- * @param convertFromVector converts from [AnimatedVector] to type [T]
+ * @param convertFromVector converts from [AnimationVector] to type [T]
  */
 fun <T, V : AnimationVector> TwoWayConverter(
     convertToVector: (T) -> V,
diff --git a/compose/animation/animation/lint-baseline.xml b/compose/animation/animation/lint-baseline.xml
deleted file mode 100644
index c8e378e..0000000
--- a/compose/animation/animation/lint-baseline.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-alpha06" client="gradle" variant="debug" version="4.2.0-alpha06">
-
-    <issue
-        id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
-        errorLine1="): Modifier = composed {"
-        errorLine2="              ~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt"
-            line="62"
-            column="15"/>
-    </issue>
-
-</issues>
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 e3d82e6..e27f2e1 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
@@ -23,11 +23,14 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.LayoutModifier
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.platform.DensityAmbient
+import androidx.compose.ui.platform.InspectableValue
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntSize
@@ -37,6 +40,11 @@
 import junit.framework.TestCase.assertEquals
 import junit.framework.TestCase.assertNotNull
 import junit.framework.TestCase.assertNull
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.nullValue
+import org.junit.After
+import org.junit.Assert.assertThat
+import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -48,6 +56,16 @@
     @get:Rule
     val rule = createComposeRule()
 
+    @Before
+    fun before() {
+        isDebugInspectorInfoEnabled = true
+    }
+
+    @After
+    fun after() {
+        isDebugInspectorInfoEnabled = false
+    }
+
     @Test
     fun animateContentSizeTest() {
         val startWidth = 100
@@ -116,6 +134,19 @@
             rule.waitForIdle()
         }
     }
+
+    @Test
+    fun testInspectorValue() {
+        rule.setContent {
+            val modifier = Modifier.animateContentSize() as InspectableValue
+            assertThat(modifier.nameFallback, `is`("animateContentSize"))
+            assertThat(modifier.valueOverride, nullValue())
+            assertThat(
+                modifier.inspectableElements.map { it.name }.toList(),
+                `is`(listOf("animSpec", "clip", "endListener"))
+            )
+        }
+    }
 }
 
 internal class TestModifier : LayoutModifier {
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
index f93a2cd..bf2a5c9 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.draw.clipToBounds
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.platform.AnimationClockAmbient
+import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntSize
 
@@ -59,7 +60,14 @@
     animSpec: AnimationSpec<IntSize> = spring(),
     clip: Boolean = true,
     endListener: ((startSize: IntSize, endSize: IntSize) -> Unit)? = null
-): Modifier = composed {
+): Modifier = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "animateContentSize"
+        properties["animSpec"] = animSpec
+        properties["clip"] = clip
+        properties["endListener"] = endListener
+    }
+) {
     // TODO: Listener could be a fun interface after 1.4
     val clock = AnimationClockAmbient.current.asDisposableClock()
     val animModifier = remember {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt
index a838b8e..3b5e65c 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt
@@ -21,6 +21,7 @@
 import android.widget.TextView
 import com.intellij.psi.PsiElement
 import com.intellij.psi.util.PsiTreeUtil
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
@@ -1945,6 +1946,7 @@
         }
     }
 
+    @Ignore("b/171801506")
     @Test
     fun testEffects3(): Unit = ensureSetup {
         val log = StringBuilder()
@@ -1988,6 +1990,7 @@
         }
     }
 
+    @Ignore("b/171801506")
     @Test
     fun testEffects4(): Unit = ensureSetup {
         val log = StringBuilder()
@@ -2333,6 +2336,7 @@
         )
     }
 
+    @Ignore("b/171801506")
     @Test
     fun testStableParameters_Various(): Unit = ensureSetup {
         val output = ArrayList<String>()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
index 607dd5a..236d89c 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
@@ -766,6 +766,7 @@
         )
     }
 
+    @Ignore("b/171801506")
     @Test
     fun testCrossModule_SimpleComposition(): Unit = ensureSetup {
         val tvId = 29
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index be57513..d5b7b43 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -30,20 +30,21 @@
     private val versionTable = mapOf(
         1600 to "0.1.0-dev16",
         1700 to "1.0.0-alpha06",
-        1800 to "1.0.0-alpha07"
+        1800 to "1.0.0-alpha07",
+        1900 to "1.0.0-alpha08"
     )
 
     /**
      * The minimum version int that this compiler is guaranteed to be compatible with. Typically
      * this will match the version int that is in ComposeVersion.kt in the runtime.
      */
-    private val minimumRuntimeVersionInt: Int = 1800
+    private val minimumRuntimeVersionInt: Int = 1900
 
     /**
      * The maven version string of this compiler. This string should be updated before/after every
      * release.
      */
-    private val compilerVersion: String = "1.0.0-alpha07"
+    private val compilerVersion: String = "1.0.0-alpha08"
     private val minimumRuntimeVersion: String
         get() = versionTable[minimumRuntimeVersionInt] ?: "unknown"
 
diff --git a/compose/foundation/foundation-layout/api/current.txt b/compose/foundation/foundation-layout/api/current.txt
index 4c8ba44..3449d52 100644
--- a/compose/foundation/foundation-layout/api/current.txt
+++ b/compose/foundation/foundation-layout/api/current.txt
@@ -73,12 +73,10 @@
   public final class BoxKt {
     method @androidx.compose.runtime.Composable public static inline void Box(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> children);
     method @androidx.compose.runtime.Composable public static void Box(androidx.compose.ui.Modifier modifier);
-    method @Deprecated @androidx.compose.runtime.Composable public static void Stack(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> children);
   }
 
   @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface BoxScope {
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier align(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment alignment);
-    method @Deprecated @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier gravity(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment align);
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier matchParentSize(androidx.compose.ui.Modifier);
     field public static final androidx.compose.foundation.layout.BoxScope.Companion Companion;
   }
@@ -110,7 +108,6 @@
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier alignBy(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
     method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, androidx.compose.ui.layout.VerticalAlignmentLine alignmentLine);
     method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
-    method @Deprecated @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier gravity(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment.Horizontal align);
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float weight, optional boolean fill);
     field public static final androidx.compose.foundation.layout.ColumnScope.Companion Companion;
   }
@@ -408,7 +405,6 @@
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier alignByBaseline(androidx.compose.ui.Modifier);
     method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, androidx.compose.ui.layout.HorizontalAlignmentLine alignmentLine);
     method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
-    method @Deprecated @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier gravity(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment.Vertical align);
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float weight, optional boolean fill);
     field public static final androidx.compose.foundation.layout.RowScope.Companion Companion;
   }
diff --git a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
index 4c8ba44..3449d52 100644
--- a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
@@ -73,12 +73,10 @@
   public final class BoxKt {
     method @androidx.compose.runtime.Composable public static inline void Box(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> children);
     method @androidx.compose.runtime.Composable public static void Box(androidx.compose.ui.Modifier modifier);
-    method @Deprecated @androidx.compose.runtime.Composable public static void Stack(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> children);
   }
 
   @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface BoxScope {
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier align(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment alignment);
-    method @Deprecated @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier gravity(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment align);
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier matchParentSize(androidx.compose.ui.Modifier);
     field public static final androidx.compose.foundation.layout.BoxScope.Companion Companion;
   }
@@ -110,7 +108,6 @@
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier alignBy(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
     method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, androidx.compose.ui.layout.VerticalAlignmentLine alignmentLine);
     method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
-    method @Deprecated @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier gravity(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment.Horizontal align);
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float weight, optional boolean fill);
     field public static final androidx.compose.foundation.layout.ColumnScope.Companion Companion;
   }
@@ -408,7 +405,6 @@
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier alignByBaseline(androidx.compose.ui.Modifier);
     method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, androidx.compose.ui.layout.HorizontalAlignmentLine alignmentLine);
     method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
-    method @Deprecated @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier gravity(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment.Vertical align);
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float weight, optional boolean fill);
     field public static final androidx.compose.foundation.layout.RowScope.Companion Companion;
   }
diff --git a/compose/foundation/foundation-layout/api/restricted_current.txt b/compose/foundation/foundation-layout/api/restricted_current.txt
index bc46e47..79fe613 100644
--- a/compose/foundation/foundation-layout/api/restricted_current.txt
+++ b/compose/foundation/foundation-layout/api/restricted_current.txt
@@ -73,13 +73,11 @@
   public final class BoxKt {
     method @androidx.compose.runtime.Composable public static inline void Box(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> children);
     method @androidx.compose.runtime.Composable public static void Box(androidx.compose.ui.Modifier modifier);
-    method @Deprecated @androidx.compose.runtime.Composable public static void Stack(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment alignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> children);
     method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.node.LayoutNode.MeasureBlocks rememberMeasureBlocks(androidx.compose.ui.Alignment alignment);
   }
 
   @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface BoxScope {
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier align(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment alignment);
-    method @Deprecated @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier gravity(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment align);
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier matchParentSize(androidx.compose.ui.Modifier);
     field public static final androidx.compose.foundation.layout.BoxScope.Companion Companion;
   }
@@ -113,7 +111,6 @@
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier alignBy(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
     method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, androidx.compose.ui.layout.VerticalAlignmentLine alignmentLine);
     method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
-    method @Deprecated @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier gravity(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment.Horizontal align);
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float weight, optional boolean fill);
     field public static final androidx.compose.foundation.layout.ColumnScope.Companion Companion;
   }
@@ -414,7 +411,6 @@
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier alignByBaseline(androidx.compose.ui.Modifier);
     method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, androidx.compose.ui.layout.HorizontalAlignmentLine alignmentLine);
     method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
-    method @Deprecated @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier gravity(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment.Vertical align);
     method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float weight, optional boolean fill);
     field public static final androidx.compose.foundation.layout.RowScope.Companion Companion;
   }
diff --git a/compose/foundation/foundation-layout/lint-baseline.xml b/compose/foundation/foundation-layout/lint-baseline.xml
index 83a7624..cf10878 100644
--- a/compose/foundation/foundation-layout/lint-baseline.xml
+++ b/compose/foundation/foundation-layout/lint-baseline.xml
@@ -3,7 +3,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="        return this.then(object : ParentDataModifier {"
         errorLine2="                         ^">
         <location
@@ -14,7 +14,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="fun Modifier.preferredWidth(intrinsicSize: IntrinsicSize) = when (intrinsicSize) {"
         errorLine2="                                                            ~~~~~~">
         <location
@@ -25,7 +25,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="fun Modifier.preferredHeight(intrinsicSize: IntrinsicSize) = when (intrinsicSize) {"
         errorLine2="                                                             ~~~~~~">
         <location
diff --git a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/ConstraintLayout.kt b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/ConstraintLayout.kt
index 6a76b59..8ec81f9 100644
--- a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/ConstraintLayout.kt
+++ b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/ConstraintLayout.kt
@@ -29,6 +29,7 @@
 import androidx.compose.ui.layout.ParentDataModifier
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.id
+import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
@@ -1087,7 +1088,7 @@
 fun ConstraintSet(description: ConstraintSetScope.() -> Unit) = object : ConstraintSet {
     override fun applyTo(state: State, measurables: List<Measurable>) {
         measurables.forEach { measurable ->
-            state.map((measurable.id ?: createId()), measurable)
+            state.map((measurable.layoutId ?: createId()), measurable)
         }
         val scope = ConstraintSetScope()
         scope.description()
@@ -1145,7 +1146,7 @@
         if (DEBUG) {
             Log.d(
                 "CCL",
-                "Measuring ${measurable.id} with: " +
+                "Measuring ${measurable.layoutId} with: " +
                     constraintWidget.toDebugString() + "\n" + measure.toDebugString()
             )
         }
@@ -1191,13 +1192,16 @@
             constraintWidget.mMatchConstraintDefaultHeight != MATCH_CONSTRAINT_SPREAD
         ) {
             if (DEBUG) {
-                Log.d("CCL", "Measuring ${measurable.id} with $constraints")
+                Log.d("CCL", "Measuring ${measurable.layoutId} with $constraints")
             }
             val placeable = with(measureScope) {
                 measurable.measure(constraints).also { placeables[measurable] = it }
             }
             if (DEBUG) {
-                Log.d("CCL", "${measurable.id} is size ${placeable.width} ${placeable.height}")
+                Log.d(
+                    "CCL",
+                    "${measurable.layoutId} is size ${placeable.width} ${placeable.height}"
+                )
             }
             if (wrappingWidth) {
                 constraintWidget.wrapMeasure[0] = placeable.width
@@ -1232,7 +1236,7 @@
             }
             if (remeasure) {
                 if (DEBUG) {
-                    Log.d("CCL", "Remeasuring coerced ${measurable.id} with $constraints")
+                    Log.d("CCL", "Remeasuring coerced ${measurable.layoutId} with $constraints")
                 }
                 with(measureScope) {
                     measurable.measure(constraints).also { placeables[measurable] = it }
@@ -1335,7 +1339,8 @@
         if (DEBUG) {
             root.debugName = "ConstraintLayout"
             root.children.forEach { child ->
-                child.debugName = (child.companionWidget as? Measurable)?.id?.toString() ?: "NOTAG"
+                child.debugName =
+                    (child.companionWidget as? Measurable)?.layoutId?.toString() ?: "NOTAG"
             }
             Log.d("CCL", "ConstraintLayout is asked to measure with $constraints")
             Log.d("CCL", root.toDebugString())
@@ -1357,7 +1362,7 @@
                 if (DEBUG) {
                     Log.d(
                         "CCL",
-                        "Final measurement for ${measurable.id} " +
+                        "Final measurement for ${measurable.layoutId} " +
                             "to confirm size ${child.width} ${child.height}"
                     )
                 }
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt
index fcbe4bb..483cf92 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt
@@ -197,17 +197,6 @@
     layout(constraints.minWidth, constraints.minHeight) {}
 }
 
-@Composable
-@Deprecated(
-    "Stack was renamed to Box.",
-    ReplaceWith("Box", "androidx.compose.foundation.layout.Box")
-)
-fun Stack(
-    modifier: Modifier = Modifier,
-    alignment: Alignment = Alignment.TopStart,
-    children: @Composable BoxScope.() -> Unit
-) = Box(modifier, alignment, children)
-
 /**
  * A BoxScope provides a scope for the children of a [Box].
  */
@@ -230,10 +219,6 @@
         )
     )
 
-    @Stable
-    @Deprecated("gravity has been renamed to align.", ReplaceWith("align(align)"))
-    fun Modifier.gravity(align: Alignment) = align(align)
-
     /**
      * Size the element to match the size of the [Box] after all other content elements have
      * been measured.
@@ -258,12 +243,6 @@
     companion object : BoxScope
 }
 
-@Deprecated(
-    "Stack was renamed to Box.",
-    ReplaceWith("BoxScope", "androidx.compose.foundation.layout.BoxScope")
-)
-typealias StackScope = BoxScope
-
 private val Measurable.boxChildData: BoxChildData? get() = parentData as? BoxChildData
 private val Measurable.matchesParentSize: Boolean get() = boxChildData?.matchParentSize ?: false
 
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Column.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Column.kt
index e864e93..202ef2e 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Column.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Column.kt
@@ -21,10 +21,10 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.VerticalAlignmentLine
+import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.Measured
+import androidx.compose.ui.layout.VerticalAlignmentLine
 import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.util.annotation.FloatRange
@@ -135,10 +135,6 @@
         )
     )
 
-    @Stable
-    @Deprecated("gravity has been renamed to align.", ReplaceWith("align(align)"))
-    fun Modifier.gravity(align: Alignment.Horizontal) = align(align)
-
     /**
      * Position the element horizontally such that its [alignmentLine] aligns with sibling elements
      * also configured to [alignBy]. [alignBy] is a form of [align],
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutPadding.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutPadding.kt
index 29b113f..8256872 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutPadding.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutPadding.kt
@@ -255,6 +255,3 @@
 ) {
     constructor(all: Dp) : this(all, all, all, all)
 }
-
-@Deprecated("InnerPadding was renamed to PaddingValues.", ReplaceWith("PaddingValues"))
-typealias InnerPadding = PaddingValues
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Row.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Row.kt
index 475e005..6364484 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Row.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Row.kt
@@ -143,10 +143,6 @@
         )
     )
 
-    @Stable
-    @Deprecated("gravity has been renamed to align.", ReplaceWith("align(align)"))
-    fun Modifier.gravity(align: Alignment.Vertical) = align(align)
-
     /**
      * Position the element vertically such that its [alignmentLine] aligns with sibling elements
      * also configured to [alignBy]. [alignBy] is a form of [align],
diff --git a/compose/foundation/foundation-text/lint-baseline.xml b/compose/foundation/foundation-text/lint-baseline.xml
index ec5b618..ee6011f 100644
--- a/compose/foundation/foundation-text/lint-baseline.xml
+++ b/compose/foundation/foundation-text/lint-baseline.xml
@@ -3,7 +3,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="    return this"
         errorLine2="           ^">
         <location
@@ -14,7 +14,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1=") = composed {"
         errorLine2="    ~~~~~~~~">
         <location
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index c8bc665..f1edd22 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -10,23 +10,10 @@
     method @Deprecated @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BaseTextField-ngE-tqQ(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional long textColor, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.text.input.KeyboardType keyboardType, optional androidx.compose.ui.text.input.ImeAction imeAction, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor);
   }
 
-  @Deprecated @androidx.compose.runtime.Immutable public final class Border {
-    method @Deprecated public float component1-D9Ej5fM();
-    method @Deprecated public androidx.compose.ui.graphics.Brush component2();
-    method @Deprecated @androidx.compose.runtime.Immutable public androidx.compose.foundation.Border copy-v_fYJzc(float size, androidx.compose.ui.graphics.Brush brush);
-    method @Deprecated public androidx.compose.ui.graphics.Brush getBrush();
-    method @Deprecated public float getSize-D9Ej5fM();
-    property public final androidx.compose.ui.graphics.Brush brush;
-    property public final float size;
-  }
-
   public final class BorderKt {
     method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, optional androidx.compose.ui.graphics.Shape shape);
     method public static androidx.compose.ui.Modifier border-bMj1UE0(androidx.compose.ui.Modifier, float width, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
     method public static androidx.compose.ui.Modifier border-zRMYNwQ(androidx.compose.ui.Modifier, float width, long color, optional androidx.compose.ui.graphics.Shape shape);
-    method @Deprecated public static androidx.compose.ui.Modifier drawBorder(androidx.compose.ui.Modifier, androidx.compose.foundation.Border border, optional androidx.compose.ui.graphics.Shape shape);
-    method @Deprecated public static androidx.compose.ui.Modifier drawBorder-bMj1UE0(androidx.compose.ui.Modifier, float size, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
-    method @Deprecated public static androidx.compose.ui.Modifier drawBorder-zRMYNwQ(androidx.compose.ui.Modifier, float size, long color, optional androidx.compose.ui.graphics.Shape shape);
   }
 
   @androidx.compose.runtime.Immutable public final class BorderStroke {
@@ -40,14 +27,9 @@
   }
 
   public final class BorderStrokeKt {
-    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.foundation.Border Border-Qek64HU(float size, long color);
     method @androidx.compose.runtime.Stable public static androidx.compose.foundation.BorderStroke BorderStroke-Qek64HU(float width, long color);
   }
 
-  public final class BoxKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static void Box-0SX22z8(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long backgroundColor, optional androidx.compose.foundation.BorderStroke? border, optional float padding, optional float paddingStart, optional float paddingTop, optional float paddingEnd, optional float paddingBottom, optional androidx.compose.ui.Alignment gravity, optional kotlin.jvm.functions.Function0<kotlin.Unit> children);
-  }
-
   public final class CanvasKt {
     method @androidx.compose.runtime.Composable public static void Canvas(androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
   }
@@ -244,6 +226,20 @@
     method public static androidx.compose.ui.Modifier draggable(androidx.compose.ui.Modifier, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.InteractionState? interactionState, optional boolean startDragImmediately, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.gesture.Direction,java.lang.Boolean> canDrag, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDragStarted, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onDragStopped, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Float,kotlin.Unit> onDrag);
   }
 
+  public final class ForEachGestureKt {
+    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+  }
+
+  public final class GestureCancellationException extends java.util.concurrent.CancellationException {
+    ctor public GestureCancellationException(String? message);
+    ctor public GestureCancellationException();
+  }
+
+  public interface PressGestureScope extends androidx.compose.ui.unit.Density {
+    method public suspend Object? awaitRelease(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public suspend Object? tryAwaitRelease(kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+  }
+
   public final class ScrollableController {
     ctor public ScrollableController(internal kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> consumeScrollDelta, internal androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock, internal androidx.compose.foundation.InteractionState? interactionState);
     method public boolean isAnimationRunning();
@@ -257,6 +253,12 @@
     method public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation, androidx.compose.foundation.gestures.ScrollableController controller, optional boolean enabled, optional boolean reverseDirection, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.gesture.Direction,java.lang.Boolean> canScroll, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onScrollStarted, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onScrollStopped);
   }
 
+  public final class TapGestureDetectorKt {
+    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? tapGestureDetector(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? waitForFirstDown(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? waitForUpOrCancel(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+  }
+
   public final class ZoomableController {
     ctor public ZoomableController(androidx.compose.animation.core.AnimationClockObservable animationClock, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onZoomDelta);
     method public kotlin.jvm.functions.Function1<java.lang.Float,kotlin.Unit> getOnZoomDelta();
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index c8bc665..f1edd22 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -10,23 +10,10 @@
     method @Deprecated @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BaseTextField-ngE-tqQ(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional long textColor, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.text.input.KeyboardType keyboardType, optional androidx.compose.ui.text.input.ImeAction imeAction, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor);
   }
 
-  @Deprecated @androidx.compose.runtime.Immutable public final class Border {
-    method @Deprecated public float component1-D9Ej5fM();
-    method @Deprecated public androidx.compose.ui.graphics.Brush component2();
-    method @Deprecated @androidx.compose.runtime.Immutable public androidx.compose.foundation.Border copy-v_fYJzc(float size, androidx.compose.ui.graphics.Brush brush);
-    method @Deprecated public androidx.compose.ui.graphics.Brush getBrush();
-    method @Deprecated public float getSize-D9Ej5fM();
-    property public final androidx.compose.ui.graphics.Brush brush;
-    property public final float size;
-  }
-
   public final class BorderKt {
     method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, optional androidx.compose.ui.graphics.Shape shape);
     method public static androidx.compose.ui.Modifier border-bMj1UE0(androidx.compose.ui.Modifier, float width, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
     method public static androidx.compose.ui.Modifier border-zRMYNwQ(androidx.compose.ui.Modifier, float width, long color, optional androidx.compose.ui.graphics.Shape shape);
-    method @Deprecated public static androidx.compose.ui.Modifier drawBorder(androidx.compose.ui.Modifier, androidx.compose.foundation.Border border, optional androidx.compose.ui.graphics.Shape shape);
-    method @Deprecated public static androidx.compose.ui.Modifier drawBorder-bMj1UE0(androidx.compose.ui.Modifier, float size, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
-    method @Deprecated public static androidx.compose.ui.Modifier drawBorder-zRMYNwQ(androidx.compose.ui.Modifier, float size, long color, optional androidx.compose.ui.graphics.Shape shape);
   }
 
   @androidx.compose.runtime.Immutable public final class BorderStroke {
@@ -40,14 +27,9 @@
   }
 
   public final class BorderStrokeKt {
-    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.foundation.Border Border-Qek64HU(float size, long color);
     method @androidx.compose.runtime.Stable public static androidx.compose.foundation.BorderStroke BorderStroke-Qek64HU(float width, long color);
   }
 
-  public final class BoxKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static void Box-0SX22z8(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long backgroundColor, optional androidx.compose.foundation.BorderStroke? border, optional float padding, optional float paddingStart, optional float paddingTop, optional float paddingEnd, optional float paddingBottom, optional androidx.compose.ui.Alignment gravity, optional kotlin.jvm.functions.Function0<kotlin.Unit> children);
-  }
-
   public final class CanvasKt {
     method @androidx.compose.runtime.Composable public static void Canvas(androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
   }
@@ -244,6 +226,20 @@
     method public static androidx.compose.ui.Modifier draggable(androidx.compose.ui.Modifier, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.InteractionState? interactionState, optional boolean startDragImmediately, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.gesture.Direction,java.lang.Boolean> canDrag, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDragStarted, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onDragStopped, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Float,kotlin.Unit> onDrag);
   }
 
+  public final class ForEachGestureKt {
+    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+  }
+
+  public final class GestureCancellationException extends java.util.concurrent.CancellationException {
+    ctor public GestureCancellationException(String? message);
+    ctor public GestureCancellationException();
+  }
+
+  public interface PressGestureScope extends androidx.compose.ui.unit.Density {
+    method public suspend Object? awaitRelease(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public suspend Object? tryAwaitRelease(kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+  }
+
   public final class ScrollableController {
     ctor public ScrollableController(internal kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> consumeScrollDelta, internal androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock, internal androidx.compose.foundation.InteractionState? interactionState);
     method public boolean isAnimationRunning();
@@ -257,6 +253,12 @@
     method public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation, androidx.compose.foundation.gestures.ScrollableController controller, optional boolean enabled, optional boolean reverseDirection, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.gesture.Direction,java.lang.Boolean> canScroll, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onScrollStarted, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onScrollStopped);
   }
 
+  public final class TapGestureDetectorKt {
+    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? tapGestureDetector(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? waitForFirstDown(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? waitForUpOrCancel(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+  }
+
   public final class ZoomableController {
     ctor public ZoomableController(androidx.compose.animation.core.AnimationClockObservable animationClock, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onZoomDelta);
     method public kotlin.jvm.functions.Function1<java.lang.Float,kotlin.Unit> getOnZoomDelta();
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index c8bc665..f1edd22 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -10,23 +10,10 @@
     method @Deprecated @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BaseTextField-ngE-tqQ(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional long textColor, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.text.input.KeyboardType keyboardType, optional androidx.compose.ui.text.input.ImeAction imeAction, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor);
   }
 
-  @Deprecated @androidx.compose.runtime.Immutable public final class Border {
-    method @Deprecated public float component1-D9Ej5fM();
-    method @Deprecated public androidx.compose.ui.graphics.Brush component2();
-    method @Deprecated @androidx.compose.runtime.Immutable public androidx.compose.foundation.Border copy-v_fYJzc(float size, androidx.compose.ui.graphics.Brush brush);
-    method @Deprecated public androidx.compose.ui.graphics.Brush getBrush();
-    method @Deprecated public float getSize-D9Ej5fM();
-    property public final androidx.compose.ui.graphics.Brush brush;
-    property public final float size;
-  }
-
   public final class BorderKt {
     method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, optional androidx.compose.ui.graphics.Shape shape);
     method public static androidx.compose.ui.Modifier border-bMj1UE0(androidx.compose.ui.Modifier, float width, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
     method public static androidx.compose.ui.Modifier border-zRMYNwQ(androidx.compose.ui.Modifier, float width, long color, optional androidx.compose.ui.graphics.Shape shape);
-    method @Deprecated public static androidx.compose.ui.Modifier drawBorder(androidx.compose.ui.Modifier, androidx.compose.foundation.Border border, optional androidx.compose.ui.graphics.Shape shape);
-    method @Deprecated public static androidx.compose.ui.Modifier drawBorder-bMj1UE0(androidx.compose.ui.Modifier, float size, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
-    method @Deprecated public static androidx.compose.ui.Modifier drawBorder-zRMYNwQ(androidx.compose.ui.Modifier, float size, long color, optional androidx.compose.ui.graphics.Shape shape);
   }
 
   @androidx.compose.runtime.Immutable public final class BorderStroke {
@@ -40,14 +27,9 @@
   }
 
   public final class BorderStrokeKt {
-    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.foundation.Border Border-Qek64HU(float size, long color);
     method @androidx.compose.runtime.Stable public static androidx.compose.foundation.BorderStroke BorderStroke-Qek64HU(float width, long color);
   }
 
-  public final class BoxKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static void Box-0SX22z8(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long backgroundColor, optional androidx.compose.foundation.BorderStroke? border, optional float padding, optional float paddingStart, optional float paddingTop, optional float paddingEnd, optional float paddingBottom, optional androidx.compose.ui.Alignment gravity, optional kotlin.jvm.functions.Function0<kotlin.Unit> children);
-  }
-
   public final class CanvasKt {
     method @androidx.compose.runtime.Composable public static void Canvas(androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
   }
@@ -244,6 +226,20 @@
     method public static androidx.compose.ui.Modifier draggable(androidx.compose.ui.Modifier, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.InteractionState? interactionState, optional boolean startDragImmediately, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.gesture.Direction,java.lang.Boolean> canDrag, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDragStarted, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onDragStopped, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Float,kotlin.Unit> onDrag);
   }
 
+  public final class ForEachGestureKt {
+    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+  }
+
+  public final class GestureCancellationException extends java.util.concurrent.CancellationException {
+    ctor public GestureCancellationException(String? message);
+    ctor public GestureCancellationException();
+  }
+
+  public interface PressGestureScope extends androidx.compose.ui.unit.Density {
+    method public suspend Object? awaitRelease(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public suspend Object? tryAwaitRelease(kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+  }
+
   public final class ScrollableController {
     ctor public ScrollableController(internal kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> consumeScrollDelta, internal androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock, internal androidx.compose.foundation.InteractionState? interactionState);
     method public boolean isAnimationRunning();
@@ -257,6 +253,12 @@
     method public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation, androidx.compose.foundation.gestures.ScrollableController controller, optional boolean enabled, optional boolean reverseDirection, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.gesture.Direction,java.lang.Boolean> canScroll, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onScrollStarted, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onScrollStopped);
   }
 
+  public final class TapGestureDetectorKt {
+    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? tapGestureDetector(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? waitForFirstDown(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? waitForUpOrCancel(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+  }
+
   public final class ZoomableController {
     ctor public ZoomableController(androidx.compose.animation.core.AnimationClockObservable animationClock, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onZoomDelta);
     method public kotlin.jvm.functions.Function1<java.lang.Float,kotlin.Unit> getOnZoomDelta();
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 093b8b2..dcf467b 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -55,6 +55,7 @@
         testImplementation(ANDROIDX_TEST_RUNNER)
         testImplementation(JUNIT)
         testImplementation(TRUTH)
+        testImplementation(KOTLIN_COROUTINES_TEST)
 
         androidTestImplementation project(":compose:test-utils")
         androidTestImplementation project(':ui:ui-test')
@@ -109,6 +110,10 @@
                 implementation(JUNIT)
             }
 
+            commonTest.dependencies {
+                implementation(KOTLIN_COROUTINES_TEST)
+            }
+
             androidAndroidTest.dependencies {
                 implementation project(":compose:test-utils")
                 implementation project(':ui:ui-test')
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
index cdeba47..ee93b8d 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
@@ -32,6 +32,9 @@
         ComposableDemo("Draw Modifiers") { DrawModifiersDemo() },
         DemoCategory("Lazy lists", LazyListDemos),
         ComposableDemo("Priority InteractionState") { PriorityInteractionStateSample() },
-        ComposableDemo("Multiple-interaction InteractionState") { MultipleInteractionStateSample() }
+        ComposableDemo("Multiple-interaction InteractionState") {
+            MultipleInteractionStateSample()
+        },
+        DemoCategory("Suspending Gesture Detectors", CoroutineGestureDemos)
     )
 )
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/SuspendingGesturesDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/SuspendingGesturesDemo.kt
new file mode 100644
index 0000000..87bcab4
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/SuspendingGesturesDemo.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.foundation.demos
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.gestures.tapGestureDetector
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.preferredHeight
+import androidx.compose.foundation.layout.preferredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.integration.demos.common.ComposableDemo
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.gesture.ExperimentalPointerInput
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.dp
+import kotlin.random.Random
+
+val CoroutineGestureDemos = listOf(
+    ComposableDemo("Tap/Double-Tap/Long Press") { CoroutineTapDemo() }
+)
+
+fun hueToColor(hue: Float): Color {
+    val huePrime = hue / 60
+    val hueRange = huePrime.toInt()
+    val hueRemainder = huePrime - hueRange
+    return when (hueRange) {
+        0 -> Color(1f, hueRemainder, 0f)
+        1 -> Color(1f - hueRemainder, 1f, 0f)
+        2 -> Color(0f, 1f, hueRemainder)
+        3 -> Color(0f, 1f - hueRemainder, 1f)
+        4 -> Color(hueRemainder, 0f, 1f)
+        else -> Color(1f, 0f, 1f - hueRemainder)
+    }
+}
+
+fun randomHue() = Random.nextFloat() * 360
+
+fun anotherRandomHue(hue: Float): Float {
+    val newHue: Float = Random.nextFloat() * 260f
+
+    // we don't want the hue to be close, so we ensure that it isn't with 50 of the current hue
+    return if (newHue > hue - 50f) {
+        newHue + 100f
+    } else {
+        newHue
+    }
+}
+/**
+ * Gesture detector for tap, double-tap, and long-press.
+ */
+@OptIn(ExperimentalPointerInput::class)
+@Composable
+fun CoroutineTapDemo() {
+    var tapHue by remember { mutableStateOf(randomHue()) }
+    var longPressHue by remember { mutableStateOf(randomHue()) }
+    var doubleTapHue by remember { mutableStateOf(randomHue()) }
+    var pressHue by remember { mutableStateOf(randomHue()) }
+    var releaseHue by remember { mutableStateOf(randomHue()) }
+    var cancelHue by remember { mutableStateOf(randomHue()) }
+
+    Column {
+        Text("The boxes change color when you tap the white box.")
+        Spacer(Modifier.size(5.dp))
+        Box(
+            Modifier
+                .fillMaxWidth()
+                .preferredHeight(50.dp)
+                .pointerInput {
+                    tapGestureDetector(
+                         tapHue = anotherRandomHue(tapHue) },
+                         doubleTapHue = anotherRandomHue(doubleTapHue) },
+                         longPressHue = anotherRandomHue(longPressHue) },
+                        >
+                            pressHue = anotherRandomHue(pressHue)
+                            if (tryAwaitRelease()) {
+                                releaseHue = anotherRandomHue(releaseHue)
+                            } else {
+                                cancelHue = anotherRandomHue(cancelHue)
+                            }
+                        }
+                    )
+                }
+                .background(Color.White)
+                .border(BorderStroke(2.dp, Color.Black))
+        ) {
+            Text("Tap, double-tap, or long-press", Modifier.align(Alignment.Center))
+        }
+        Spacer(Modifier.size(5.dp))
+        Row {
+            Box(
+                Modifier
+                    .preferredSize(50.dp)
+                    .background(hueToColor(tapHue))
+                    .border(BorderStroke(2.dp, Color.Black))
+            )
+            Text("Changes color on tap", Modifier.align(Alignment.CenterVertically))
+        }
+        Spacer(Modifier.size(5.dp))
+        Row {
+            Box(
+                Modifier
+                    .preferredSize(50.dp)
+                    .clipToBounds()
+                    .background(hueToColor(doubleTapHue))
+                    .border(BorderStroke(2.dp, Color.Black))
+            )
+            Text("Changes color on double-tap", Modifier.align(Alignment.CenterVertically))
+        }
+        Spacer(Modifier.size(5.dp))
+        Row {
+            Box(
+                Modifier
+                    .preferredSize(50.dp)
+                    .clipToBounds()
+                    .background(hueToColor(longPressHue))
+                    .border(BorderStroke(2.dp, Color.Black))
+            )
+            Text("Changes color on long press", Modifier.align(Alignment.CenterVertically))
+        }
+        Spacer(Modifier.size(5.dp))
+        Row {
+            Box(
+                Modifier
+                    .preferredSize(50.dp)
+                    .clipToBounds()
+                    .background(hueToColor(pressHue))
+                    .border(BorderStroke(2.dp, Color.Black))
+            )
+            Text("Changes color on press", Modifier.align(Alignment.CenterVertically))
+        }
+        Spacer(Modifier.size(5.dp))
+        Row {
+            Box(
+                Modifier
+                    .preferredSize(50.dp)
+                    .clipToBounds()
+                    .background(hueToColor(releaseHue))
+                    .border(BorderStroke(2.dp, Color.Black))
+            )
+            Text("Changes color on release", Modifier.align(Alignment.CenterVertically))
+        }
+        Spacer(Modifier.size(5.dp))
+        Row {
+            Box(
+                Modifier
+                    .preferredSize(50.dp)
+                    .clipToBounds()
+                    .background(hueToColor(cancelHue))
+                    .border(BorderStroke(2.dp, Color.Black))
+            )
+            Text("Changes color on cancel", Modifier.align(Alignment.CenterVertically))
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
index 833bb8e..06ce451 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
@@ -35,6 +35,7 @@
 import androidx.compose.ui.graphics.Paint
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
 import androidx.compose.ui.graphics.painter.ImagePainter
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.layout.ContentScale
@@ -47,7 +48,9 @@
 import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.compose.testutils.assertPixels
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
@@ -293,6 +296,46 @@
     }
 
     @Test
+    fun testImageScalesNonuniformly() {
+        val imageComposableWidth = imageWidth * 3
+        val imageComposableHeight = imageHeight * 7
+
+        rule.setContent {
+            val density = DensityAmbient.current
+            val size = (containerSize * 2 / density.density).dp
+            val imageAsset = ImageAsset(imageWidth, imageHeight)
+            CanvasDrawScope().draw(
+                density,
+                LayoutDirection.Ltr,
+                Canvas(imageAsset),
+                Size(imageWidth.toFloat(), imageHeight.toFloat())
+            ) {
+                drawRect(color = Color.Blue)
+            }
+            Box(
+                Modifier.preferredSize(size)
+                    .background(color = Color.White)
+                    .wrapContentSize(Alignment.Center)
+            ) {
+                Image(
+                    asset = imageAsset,
+                    modifier = Modifier
+                        .testTag(contentTag)
+                        .preferredSize(
+                            (imageComposableWidth / density.density).dp,
+                            (imageComposableHeight / density.density).dp
+                        ),
+                    // Scale the image non-uniformly within the bounds of the composable
+                    contentScale = ContentScale.FillBounds,
+                    alignment = Alignment.BottomEnd
+                )
+            }
+        }
+
+        rule.onNodeWithTag(contentTag).captureToImage().assertPixels { Color.Blue }
+    }
+
+    @Test
     fun testImageFixedSizeAlignedBottomEnd() {
         val imageComposableWidth = imageWidth * 2
         val imageComposableHeight = imageHeight * 2
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldTest.kt
index b5d7c81..b431418 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldTest.kt
@@ -21,6 +21,7 @@
 
 import android.os.Build
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.preferredSize
 import androidx.compose.foundation.layout.width
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Border.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Border.kt
index 1fe8ace..e297b9d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Border.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Border.kt
@@ -50,7 +50,7 @@
  *
  * @sample androidx.compose.foundation.samples.BorderSample()
  *
- * @param border [Border] class that specifies border appearance, such as size and color
+ * @param border [BorderStroke] class that specifies border appearance, such as size and color
  * @param shape shape of the border
  */
 fun Modifier.border(border: BorderStroke, shape: Shape = RectangleShape) =
@@ -94,73 +94,6 @@
     }
 )
 
-/**
- * Returns a [Modifier] that adds border with appearance specified with a [border] and a [shape]
- *
- * @sample androidx.compose.foundation.samples.BorderSample()
- *
- * @param border [Border] class that specifies border appearance, such as size and color
- * @param shape shape of the border
- */
-@Deprecated(
-    "Use Modifier.border instead",
-    replaceWith = ReplaceWith(
-        "this.border(BorderStroke(border.size, border.brush), shape)",
-        "androidx.ui.foundation.border"
-    )
-)
-@Suppress("DEPRECATION")
-fun Modifier.drawBorder(border: Border, shape: Shape = RectangleShape) =
-    drawBorder(size = border.size, brush = border.brush, shape = shape)
-
-/**
- * Returns a [Modifier] that adds border with appearance specified with [size], [color] and a
- * [shape]
- *
- * @sample androidx.compose.foundation.samples.BorderSampleWithDataClass()
- *
- * @param size width of the border. Use [Dp.Hairline] for a hairline border.
- * @param color color to paint the border with
- * @param shape shape of the border
- */
-@Deprecated(
-    "Use Modifier.border instead",
-    replaceWith = ReplaceWith(
-        "this.border(size, color, shape)",
-        "androidx.ui.foundation.border"
-    )
-)
-@Suppress("DEPRECATION")
-fun Modifier.drawBorder(size: Dp, color: Color, shape: Shape = RectangleShape) =
-    border(size, SolidColor(color), shape)
-
-/**
- * Returns a [Modifier] that adds border with appearance specified with [size], [brush] and a
- * [shape]
- *
- * @sample androidx.compose.foundation.samples.BorderSampleWithBrush()
- *
- * @param size width of the border. Use [Dp.Hairline] for a hairline border.
- * @param brush brush to paint the border with
- * @param shape shape of the border
- */
-@Deprecated(
-    "Use Modifier.border instead",
-    replaceWith = ReplaceWith(
-        "this.border(size, brush, shape)",
-        "androidx.ui.foundation.border"
-    )
-)
-fun Modifier.drawBorder(size: Dp, brush: Brush, shape: Shape): Modifier = composed(
-    factory = { BorderModifier(remember { BorderModifierCache() }, shape, size, brush) },
-    inspectorInfo = debugInspectorInfo {
-        name = "drawBorder"
-        properties["size"] = size
-        properties["brush"] = brush
-        properties["shape"] = shape
-    }
-)
-
 private class BorderModifier(
     private val cache: BorderModifierCache,
     private val shape: Shape,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BorderStroke.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BorderStroke.kt
index 55b4a5c..72c0e02 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BorderStroke.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BorderStroke.kt
@@ -39,37 +39,4 @@
  * @param color color to paint the border with
  */
 @Stable
-fun BorderStroke(width: Dp, color: Color) = BorderStroke(width, SolidColor(color))
-
-/**
- * Class to specify border appearance.
- *
- * @param size size of the border in [Dp]. Use [Dp.Hairline] for one-pixel border.
- * @param brush brush to paint the border with
- */
-@Immutable
-@Deprecated(
-    "Use BorderStroke instead",
-    replaceWith = ReplaceWith(
-        "BorderStroke(size, brush)",
-        "androidx.ui.foundation.BorderStroke"
-    )
-)
-data class Border(val size: Dp, val brush: Brush)
-
-/**
- * Create [Border] class with size and [Color]
- *
- * @param size size of the border in [Dp]. Use [Dp.Hairline] for one-pixel border.
- * @param color color to paint the border with
- */
-@Stable
-@Deprecated(
-    "Use BorderStroke instead",
-    replaceWith = ReplaceWith(
-        "BorderStroke(size, color)",
-        "androidx.ui.foundation.BorderStroke"
-    )
-)
-@Suppress("DEPRECATION")
-fun Border(size: Dp, color: Color) = Border(size, SolidColor(color))
\ No newline at end of file
+fun BorderStroke(width: Dp, color: Color) = BorderStroke(width, SolidColor(color))
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Box.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Box.kt
deleted file mode 100644
index d5777a2..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Box.kt
+++ /dev/null
@@ -1,131 +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.compose.foundation
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.emptyContent
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-
-@Deprecated(
-    "All Box parameters have been removed (and gravity has been renamed to alignment). Use " +
-        "Modifier.background, Modifier.border and Modifier.padding instead. Also Box has been" +
-        " moved to androidx.compose.foundation.layout and that should be used instead.",
-    replaceWith = ReplaceWith(
-        "Box(\n" +
-            "        modifier" +
-            "           .background(backgroundColor, shape)" +
-            "           .border(border, shape)" +
-            "           .padding(" +
-            "               start = if (paddingStart != Dp.Unspecified) paddingStart else " +
-            "padding," +
-            "               top = if (paddingTop != Dp.Unspecified) paddingTop else padding," +
-            "               end = if (paddingEnd != Dp.Unspecified) paddingEnd else padding," +
-            "               bottom = if (paddingBottom != Dp.Unspecified) paddingBottom else " +
-            "padding" +
-            "           ),\n" +
-            "        gravity,\n" +
-            "        children\n" +
-            "    )",
-        "androidx.compose.foundation.layout.Box",
-        "androidx.compose.foundation.background",
-        "androidx.compose.foundation.border",
-        "androidx.compose.foundation.layout.padding",
-        "androidx.compose.ui.unit.Dp"
-    )
-)
-@Suppress("DEPRECATION")
-@Composable
-fun Box(
-    modifier: Modifier = Modifier,
-    shape: Shape = RectangleShape,
-    backgroundColor: Color = Color.Transparent,
-    border: BorderStroke? = null,
-    padding: Dp = border?.width ?: 0.dp,
-    paddingStart: Dp = Dp.Unspecified,
-    paddingTop: Dp = Dp.Unspecified,
-    paddingEnd: Dp = Dp.Unspecified,
-    paddingBottom: Dp = Dp.Unspecified,
-    gravity: ContentGravity = ContentGravity.TopStart,
-    children: @Composable () -> Unit = emptyContent()
-) {
-    val columnArrangement = gravity.toColumnArrangement()
-    val columnGravity = gravity.toColumnGravity()
-
-    Column(
-        modifier
-            .then(
-                if (backgroundColor != Color.Transparent) {
-                    Modifier.background(color = backgroundColor, shape = shape)
-                } else {
-                    Modifier
-                }
-            )
-            .then(if (border != null) Modifier.border(border, shape) else Modifier)
-            .then(
-                if (needsPadding(padding, paddingStart, paddingTop, paddingEnd, paddingBottom)) {
-                    Modifier.padding(
-                        if (paddingStart != Dp.Unspecified) paddingStart else padding,
-                        if (paddingTop != Dp.Unspecified) paddingTop else padding,
-                        if (paddingEnd != Dp.Unspecified) paddingEnd else padding,
-                        if (paddingBottom != Dp.Unspecified) paddingBottom else padding
-                    )
-                } else {
-                    Modifier
-                }
-            ),
-        verticalArrangement = columnArrangement,
-        horizontalAlignment = columnGravity
-    ) {
-        children()
-    }
-}
-
-@Deprecated("Use Alignment instead", replaceWith = ReplaceWith("Alignment"))
-typealias ContentGravity = Alignment
-
-private fun needsPadding(
-    padding: Dp,
-    paddingStart: Dp,
-    paddingTop: Dp,
-    paddingEnd: Dp,
-    paddingBottom: Dp
-) = (padding != Dp.Unspecified && padding != 0.dp) ||
-    (paddingStart != Dp.Unspecified && paddingStart != 0.dp) ||
-    (paddingTop != Dp.Unspecified && paddingTop != 0.dp) ||
-    (paddingEnd != Dp.Unspecified && paddingEnd != 0.dp) ||
-    (paddingBottom != Dp.Unspecified && paddingBottom != 0.dp)
-
-private fun Alignment.toColumnArrangement() = Arrangement.aligned(object : Alignment.Vertical {
-    override fun align(size: Int): Int = align(IntSize(0, size)).y
-})
-
-private fun Alignment.toColumnGravity(): Alignment.Horizontal = object : Alignment.Horizontal {
-    override fun align(size: Int, layoutDirection: LayoutDirection): Int {
-        return align(IntSize(size, 0), layoutDirection).x
-    }
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
new file mode 100644
index 0000000..b116305
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.foundation.gestures
+
+import androidx.compose.ui.gesture.ExperimentalPointerInput
+import androidx.compose.ui.input.pointer.HandlePointerInputScope
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.util.fastAny
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.NonCancellable.isActive
+import kotlin.coroutines.cancellation.CancellationException
+
+/**
+ * A gesture was canceled and cannot continue, likely because another gesture has taken
+ * over the pointer input stream.
+ */
+@OptIn(ExperimentalStdlibApi::class)
+class GestureCancellationException(message: String? = null) : CancellationException(message)
+
+/**
+ * Repeatedly calls [block] to handle gestures. If there is a [CancellationException],
+ * it will wait until all pointers are raised before another gesture is detected, or it
+ * exits if [isActive] is `false`.
+ */
+@OptIn(InternalCoroutinesApi::class, ExperimentalStdlibApi::class)
+@ExperimentalPointerInput
+suspend fun PointerInputScope.forEachGesture(block: suspend PointerInputScope.() -> Unit) {
+    while (isActive) {
+        try {
+            block()
+
+            // Wait for all pointers to be up. Gestures start when a finger goes down.
+            awaitAllPointersUp()
+        } catch (e: CancellationException) {
+            // The gesture was canceled. Wait for all fingers to be "up" before looping again.
+            if (isActive) {
+                awaitAllPointersUp()
+                throw e
+            }
+        }
+    }
+}
+
+/**
+ * Returns `true` if the current state of the pointer events has all pointers up and `false`
+ * if any of the pointers are down.
+ */
+@ExperimentalPointerInput
+internal fun HandlePointerInputScope.allPointersUp(): Boolean = !currentPointers.fastAny { it.down }
+
+/**
+ * Waits for all pointers to be up before returning.
+ */
+@ExperimentalPointerInput
+internal suspend fun PointerInputScope.awaitAllPointersUp() {
+    handlePointerInput { awaitAllPointersUp() }
+}
+
+/**
+ * Waits for all pointers to be up before returning.
+ */
+@ExperimentalPointerInput
+internal suspend fun HandlePointerInputScope.awaitAllPointersUp() {
+    if (!allPointersUp()) {
+        do {
+            val events = awaitPointerEvent(PointerEventPass.Final)
+        } while (events.changes.fastAny { it.current.down })
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
new file mode 100644
index 0000000..14d75ca
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
@@ -0,0 +1,311 @@
+/*
+ * 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.foundation.gestures
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.ExperimentalPointerInput
+import androidx.compose.ui.input.pointer.HandlePointerInputScope
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.anyPositionChangeConsumed
+import androidx.compose.ui.input.pointer.changedToDown
+import androidx.compose.ui.input.pointer.changedToUp
+import androidx.compose.ui.input.pointer.consumeAllChanges
+import androidx.compose.ui.input.pointer.consumeDownChange
+import androidx.compose.ui.input.pointer.isOutOfBounds
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Uptime
+import androidx.compose.ui.unit.inMilliseconds
+import androidx.compose.ui.unit.seconds
+import androidx.compose.ui.util.fastAll
+import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastForEach
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.withTimeout
+import kotlinx.coroutines.withTimeoutOrNull
+
+/**
+ * Receiver scope for [tapGestureDetector]'s `onPress` lambda. This offers
+ * two methods to allow waiting for the press to be released.
+ */
+interface PressGestureScope : Density {
+    /**
+     * Waits for the press to be released before returning. If the gesture was canceled by
+     * motion being consumed by another gesture, [GestureCancellationException] will be
+     * thrown.
+     */
+    suspend fun awaitRelease()
+
+    /**
+     * Waits for the press to be released before returning. If the press was released,
+     * `false` is returned, or if the gesture was canceled by motion being consumed by
+     * another gesture, `false` is returned .
+     */
+    suspend fun tryAwaitRelease(): Boolean
+}
+
+private val NoPressGesture: suspend PressGestureScope.(Offset) -> Unit = { }
+
+/**
+ * Detects tap, double-tap, and long press gestures and calls [onTap], [onDoubleTap], and
+ * [onLongPress], respectively, when detected. [onPress] is called when the press is detected
+ * and the [PressGestureScope.tryAwaitRelease] and [PressGestureScope.awaitRelease] can be
+ * used to detect when pointers have released or the gesture was canceled.
+ * The first pointer down and final pointer up are consumed, and in the
+ * case of long press, all changes after the long press is detected are consumed.
+ *
+ * When [onDoubleTap] is provided, the tap gesture is detected only after
+ * the [ViewConfiguration.doubleTapMinTime] has passed and [onDoubleTap] is called if the second
+ * tap is started before [ViewConfiguration.doubleTapTimeout]. If [onDoubleTap] is not provided,
+ * then [onTap] is called when the pointer up has been received.
+ *
+ * If the first down event was consumed, the entire gesture will be skipped, including
+ * [onPress]. If the first down event was not consumed, if any other gesture consumes the down or
+ * up events, the pointer moves out of the input area, or the position change is consumed,
+ * the gestures are considered canceled. [onDoubleTap], [onLongPress], and [onTap] will not be
+ * called after a gesture has been canceled.
+ */
+@ExperimentalPointerInput
+suspend fun PointerInputScope.tapGestureDetector(
+    onDoubleTap: (() -> Unit)? = null,
+    onLongPress: (() -> Unit)? = null,
+    onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
+    onTap: () -> Unit
+) {
+    val pressScope = PressGestureScopeImpl(this)
+    forEachGesture {
+        coroutineScope {
+            pressScope.reset()
+            val down = handlePointerInput {
+                waitForFirstDown().also {
+                    it.consumeDownChange()
+                }
+            }
+            if (onPress !== NoPressGesture) {
+                launch { pressScope.onPress(down.current.position!!) }
+            }
+
+            val longPressTimeout =
+                if ( null) {
+                    Int.MAX_VALUE.seconds
+                } else {
+                    viewConfiguration.longPressTimeout
+                }
+
+            var up: PointerInputChange? = null
+            try {
+                // wait for first tap up or long press
+                up = withTimeout(longPressTimeout.inMilliseconds()) {
+                    handlePointerInput {
+                        waitForUpOrCancel()?.also { it.consumeDownChange() }
+                    }
+                }
+                if (up == null) {
+                    pressScope.cancel() // tap-up was canceled
+                } else {
+                    pressScope.release()
+                }
+            } catch (_: TimeoutCancellationException) {
+                onLongPress?.invoke()
+                consumeAllEventsUntilUp()
+                pressScope.release()
+            }
+
+            if (up != null) {
+                // tap was successful.
+                if ( null) {
+                    onTap() // no need to check for double-tap.
+                } else {
+                    // check for second tap
+                    val secondDown = detectSecondTapDown(up.current.uptime!!)
+
+                    if (secondDown == null) {
+                        onTap() // no valid second tap started
+                    } else {
+                        // Second tap down detected
+                        secondDown.consumeDownChange()
+                        pressScope.reset()
+                        if (onPress !== NoPressGesture) {
+                            launch { pressScope.onPress(secondDown.current.position!!) }
+                        }
+
+                        try {
+                            // Might have a long second press as the second tap
+                            withTimeout(longPressTimeout.inMilliseconds()) {
+                                handlePointerInput {
+                                    val secondUp = waitForUpOrCancel()
+                                    if (secondUp == null) {
+                                        pressScope.cancel()
+                                        onTap()
+                                    } else {
+                                        secondUp.consumeDownChange()
+                                        pressScope.release()
+                                        onDoubleTap()
+                                    }
+                                }
+                            }
+                        } catch (e: TimeoutCancellationException) {
+                            // The first tap was valid, but the second tap is a long press.
+                            // notify for the first tap
+                            onTap()
+
+                            // notify for the long press
+                            onLongPress?.invoke()
+                            consumeAllEventsUntilUp()
+                            pressScope.release()
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+/**
+ * Reads events until the first down is received and it isn't consumed in the
+ * [PointerEventPass.Main] pass.
+ */
+@ExperimentalPointerInput
+suspend fun HandlePointerInputScope.waitForFirstDown(): PointerInputChange {
+    var event: PointerEvent
+    do {
+        event = awaitPointerEvent()
+    } while (!event.changes.fastAll { it.changedToDown() })
+    return event.changes[0]
+}
+
+/**
+ * Reads events until all pointers are up or the gesture was canceled. The gesture
+ * is considered canceled when a pointer leaves the event region, a position change
+ * has been consumed or a pointer down change event was consumed in the [PointerEventPass.Main]
+ * pass. If the gesture was not canceled, the final up change is returned or `null` if the
+ * event was canceled.
+ */
+@ExperimentalPointerInput
+suspend fun HandlePointerInputScope.waitForUpOrCancel(): PointerInputChange? {
+    while (true) {
+        val event = awaitPointerEvent(PointerEventPass.Main)
+        if (event.changes.fastAll { it.changedToUp() }) {
+            // All pointers are up
+            return event.changes[0]
+        }
+
+        if (event.changes.fastAny { it.consumed.downChange || it.isOutOfBounds(size) }) {
+            return null // Canceled
+        }
+
+        // Check for cancel by position consumption. We can look on the Final pass of the
+        // existing pointer event because it comes after the Main pass we checked above.
+        val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
+        if (consumeCheck.changes.fastAny { it.anyPositionChangeConsumed() }) {
+            return null
+        }
+    }
+}
+
+/**
+ * Consumes all event changes in the [PointerEventPass.Initial] until all pointers are up.
+ */
+@ExperimentalPointerInput
+private suspend fun PointerInputScope.consumeAllEventsUntilUp() {
+    handlePointerInput {
+        if (!allPointersUp()) {
+            do {
+                val event = awaitPointerEvent(PointerEventPass.Initial)
+                event.changes.fastForEach { it.consumeAllChanges() }
+            } while (event.changes.fastAny { it.current.down })
+        }
+    }
+}
+
+/**
+ * Reads input for second tap down event. If the second tap is within
+ * [ViewConfiguration.doubleTapMinTime] of [upTime], the event is discarded. If the second down is
+ * not detected within [ViewConfiguration.doubleTapTimeout] of [upTime], `null` is returned.
+ * Otherwise, the down event is returned.
+ */
+@ExperimentalPointerInput
+private suspend fun PointerInputScope.detectSecondTapDown(
+    upTime: Uptime
+): PointerInputChange? {
+    return withTimeoutOrNull(viewConfiguration.doubleTapTimeout.inMilliseconds()) {
+        handlePointerInput {
+            val minUptime = upTime + viewConfiguration.doubleTapMinTime
+            var change: PointerInputChange
+            // The second tap doesn't count if it happens before DoubleTapMinTime of the first tap
+            do {
+                change = waitForFirstDown()
+            } while (change.current.uptime!! < minUptime)
+            change
+        }
+    }
+}
+
+/**
+ * [tapGestureDetector]'s implementation of [PressGestureScope].
+ */
+private class PressGestureScopeImpl(
+    density: Density
+) : PressGestureScope, Density by density {
+    private var isReleased = false
+    private var isCanceled = false
+    private val mutex = Mutex(locked = false)
+
+    /**
+     * Called when a gesture has been canceled.
+     */
+    fun cancel() {
+        isCanceled = true
+        mutex.unlock()
+    }
+
+    /**
+     * Called when all pointers are up.
+     */
+    fun release() {
+        isReleased = true
+        mutex.unlock()
+    }
+
+    /**
+     * Called when a new gesture has started.
+     */
+    fun reset() {
+        mutex.tryLock() // If tryAwaitRelease wasn't called, this will be unlocked.
+        isReleased = false
+        isCanceled = false
+    }
+
+    override suspend fun awaitRelease() {
+        if (!tryAwaitRelease()) {
+            throw GestureCancellationException("The press gesture was canceled.")
+        }
+    }
+
+    override suspend fun tryAwaitRelease(): Boolean {
+        if (!isReleased && !isCanceled) {
+            mutex.lock()
+        }
+        return isReleased
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
new file mode 100644
index 0000000..47539f6
--- /dev/null
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
@@ -0,0 +1,479 @@
+/*
+ * 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.foundation.gestures
+
+import androidx.compose.runtime.Applier
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composer
+import androidx.compose.runtime.ExperimentalComposeApi
+import androidx.compose.runtime.InternalComposeApi
+import androidx.compose.runtime.Providers
+import androidx.compose.runtime.Recomposer
+import androidx.compose.runtime.SlotTable
+import androidx.compose.runtime.currentComposer
+import androidx.compose.runtime.dispatch.MonotonicFrameClock
+import androidx.compose.runtime.withRunningRecomposer
+import androidx.compose.ui.DrawLayerModifier
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.autofill.Autofill
+import androidx.compose.ui.autofill.AutofillTree
+import androidx.compose.ui.focus.ExperimentalFocus
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.ExperimentalPointerInput
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.hapticfeedback.HapticFeedback
+import androidx.compose.ui.input.key.ExperimentalKeyInput
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.pointer.ConsumedData
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerId
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputData
+import androidx.compose.ui.input.pointer.PointerInputFilter
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.materialize
+import androidx.compose.ui.node.ExperimentalLayoutNodeApi
+import androidx.compose.ui.node.InternalCoreApi
+import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.OwnedLayer
+import androidx.compose.ui.node.Owner
+import androidx.compose.ui.node.OwnerScope
+import androidx.compose.ui.platform.ClipboardManager
+import androidx.compose.ui.platform.DensityAmbient
+import androidx.compose.ui.platform.TextToolbar
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.platform.ViewConfigurationAmbient
+import androidx.compose.ui.semantics.SemanticsOwner
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.input.TextInputService
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Duration
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.Uptime
+import androidx.compose.ui.unit.milliseconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.yield
+
+/**
+ * Manages suspending pointer input for a single gesture detector, passed in
+ * [gestureDetector]. The [width] and [height] of the LayoutNode may
+ * be provided.
+ */
+@OptIn(ExperimentalPointerInput::class)
+internal class SuspendingGestureTestUtil(
+    val width: Int = 10,
+    val height: Int = 10,
+    private val gestureDetector: suspend PointerInputScope.() -> Unit,
+) {
+    private var nextPointerId = 0L
+    private val activePointers = mutableMapOf<PointerId, PointerInputChange>()
+    private var pointerInputFilter: PointerInputFilter? = null
+    private var lastTime = Duration.Zero
+    private var isExecuting = false
+
+    /**
+     * Executes the block in composition, creating a gesture detector from
+     * [gestureDetector]. The [down], [moveTo], and [up] can then be
+     * called within [block].
+     *
+     * This is not reentrant.
+     */
+    @OptIn(ExperimentalCoroutinesApi::class)
+    fun executeInComposition(block: suspend SuspendingGestureTestUtil.() -> Unit) {
+        check(!isExecuting) { "executeInComposition is not reentrant" }
+        try {
+            isExecuting = true
+            runBlockingTest {
+                val frameClock = TestFrameClock()
+
+                withContext(frameClock) {
+                    composeGesture(block)
+                }
+            }
+        } finally {
+            isExecuting = false
+            pointerInputFilter = null
+            lastTime = Duration.Zero
+            activePointers.clear()
+        }
+    }
+
+    private suspend fun composeGesture(block: suspend SuspendingGestureTestUtil.() -> Unit) {
+        withRunningRecomposer { recomposer ->
+            compose(recomposer) {
+                Providers(
+                    DensityAmbient provides Density(1f),
+                    ViewConfigurationAmbient provides TestViewConfiguration()
+                ) {
+                    pointerInputFilter = currentComposer
+                        .materialize(Modifier.pointerInput(gestureDetector)) as
+                        PointerInputFilter
+                    LayoutNode(0, 0, width, height, pointerInputFilter!! as Modifier)
+                }
+            }
+            yield()
+            block()
+        }
+    }
+
+    /**
+     * Creates a new pointer being down at [timeDiff] from the previous event. The position
+     * [x], [y] is used for the touch point. The [PointerInputChange] may be mutated
+     * prior to invoking the change on all passes in [also], if provided. All other "down"
+     * pointers will also be included in the change event.
+     */
+    suspend fun down(
+        x: Float,
+        y: Float,
+        timeDiff: Duration = 10.milliseconds,
+        also: PointerInputChange.() -> Unit = {}
+    ): PointerInputChange {
+        lastTime += timeDiff
+        val change = PointerInputChange(
+            PointerId(nextPointerId++),
+            PointerInputData(
+                Uptime.Boot + lastTime,
+                Offset(x, y),
+                true
+            ),
+            PointerInputData(null, null, false),
+            ConsumedData(Offset.Zero, false)
+        )
+        also(change)
+        activePointers[change.id] = change
+        invokeOverAllPasses()
+        return change
+    }
+
+    /**
+     * Raises the pointer. [also] will be called on the [PointerInputChange] prior to the
+     * event being invoked on all passes. After [up], the event will no longer participate
+     * in other events. [timeDiff] indicates the [Duration] from the previous event that
+     * the [up] takes place.
+     */
+    suspend fun PointerInputChange.up(
+        timeDiff: Duration = 10.milliseconds,
+        also: PointerInputChange.() -> Unit = {}
+    ): PointerInputChange {
+        lastTime += timeDiff
+        val change = copy(
+            previous = current,
+            current = PointerInputData(
+                Uptime.Boot + lastTime,
+                null,
+                false
+            ),
+            consumed = ConsumedData()
+        )
+        also(change)
+        activePointers[change.id] = change
+        invokeOverAllPasses()
+        activePointers.remove(change.id)
+        return change
+    }
+
+    /**
+     * Moves an existing [down] pointer to a new position at [timeDiff] from the most recent
+     * event. [also] will be called on the [PointerInputChange] prior to invoking the event
+     * on all passes.
+     */
+    suspend fun PointerInputChange.moveTo(
+        x: Float,
+        y: Float,
+        timeDiff: Duration = 10.milliseconds,
+        also: PointerInputChange.() -> Unit = {}
+    ): PointerInputChange {
+        lastTime += timeDiff
+        val change = copy(
+            previous = current,
+            current = PointerInputData(
+                Uptime.Boot + lastTime,
+                Offset(x, y),
+                true
+            ),
+            consumed = ConsumedData()
+        )
+        also(change)
+        activePointers[change.id] = change
+        invokeOverAllPasses()
+        return change
+    }
+
+    /**
+     * Updates all changes so that all events are at the current time.
+     */
+    private fun updateCurrentTime() {
+        val currentTime = Uptime.Boot + lastTime
+        activePointers.entries.forEach { entry ->
+            val change = entry.value
+            if (change.current.uptime != currentTime) {
+                entry.setValue(
+                    change.copy(
+                        previous = change.current,
+                        current = change.current.copy(uptime = currentTime),
+                        consumed = ConsumedData()
+                    )
+                )
+            }
+        }
+    }
+
+    /**
+     * Invokes events for all passes.
+     */
+    private suspend fun invokeOverAllPasses() {
+        updateCurrentTime()
+        val event = PointerEvent(activePointers.values.toList())
+        val size = IntSize(width, height)
+        pointerInputFilter?.onPointerEvent(event, PointerEventPass.Initial, size)
+        yield()
+        pointerInputFilter?.onPointerEvent(event, PointerEventPass.Main, size)
+        yield()
+        pointerInputFilter?.onPointerEvent(event, PointerEventPass.Final, size)
+        yield()
+    }
+
+    @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
+    private fun compose(
+        recomposer: Recomposer,
+        block: @Composable () -> Unit
+    ): Composer<Unit> {
+        return Composer(
+            SlotTable(),
+            EmptyApplier(),
+            recomposer
+        ).apply {
+            composeInitial {
+                @Suppress("UNCHECKED_CAST")
+                val fn = block as (Composer<*>, Int) -> Unit
+                fn(this, 0)
+            }
+            applyChanges()
+            slotTable.verifyWellFormed()
+        }
+    }
+
+    @Suppress("SameParameterValue")
+    @OptIn(ExperimentalLayoutNodeApi::class)
+    private fun LayoutNode(x: Int, y: Int, x2: Int, y2: Int, modifier: Modifier = Modifier) =
+        LayoutNode().apply {
+            this.modifier = modifier
+            measureBlocks = object : LayoutNode.NoIntrinsicsMeasureBlocks("not supported") {
+                override fun measure(
+                    measureScope: MeasureScope,
+                    measurables: List<Measurable>,
+                    constraints: Constraints
+                ): MeasureResult =
+                    measureScope.layout(x2 - x, y2 - y) {}
+            }
+            attach(MockOwner())
+            measure(Constraints.fixed(x2 - x, y2 - y))
+            place(x, y)
+        }
+
+    internal class TestFrameClock : MonotonicFrameClock {
+
+        private val frameCh = Channel<Long>()
+
+        @Suppress("unused")
+        suspend fun frame(frameTimeNanos: Long) {
+            frameCh.send(frameTimeNanos)
+        }
+
+        override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
+            onFrame(frameCh.receive())
+    }
+
+    @OptIn(
+        ExperimentalFocus::class,
+        ExperimentalLayoutNodeApi::class,
+        InternalCoreApi::class
+    )
+    private class MockOwner(
+        val position: IntOffset = IntOffset.Zero,
+        override val root: LayoutNode = LayoutNode()
+    ) : Owner {
+        val >
+        val >
+        val >
+
+        override val hapticFeedBack: HapticFeedback
+            get() = TODO("Not yet implemented")
+        override val clipboardManager: ClipboardManager
+            get() = TODO("Not yet implemented")
+        override val textToolbar: TextToolbar
+            get() = TODO("Not yet implemented")
+        override val autofillTree: AutofillTree
+            get() = TODO("Not yet implemented")
+        override val autofill: Autofill?
+            get() = TODO("Not yet implemented")
+        override val density: Density
+            get() = Density(1f)
+        override val semanticsOwner: SemanticsOwner
+            get() = TODO("Not yet implemented")
+        override val textInputService: TextInputService
+            get() = TODO("Not yet implemented")
+        override val focusManager: FocusManager
+            get() = TODO("Not yet implemented")
+        override val fontLoader: Font.ResourceLoader
+            get() = TODO("Not yet implemented")
+        override val layoutDirection: LayoutDirection
+            get() = LayoutDirection.Ltr
+        override var showLayoutBounds: Boolean = false
+
+        override fun onRequestMeasure(layoutNode: LayoutNode) {
+            onRequestMeasureParams += layoutNode
+        }
+
+        override fun onRequestRelayout(layoutNode: LayoutNode) {
+        }
+
+        override val hasPendingMeasureOrLayout = false
+
+        override fun onAttach(node: LayoutNode) {
+            onAttachParams += node
+        }
+
+        override fun onDetach(node: LayoutNode) {
+            onDetachParams += node
+        }
+
+        override fun calculatePosition(): IntOffset = position
+
+        override fun requestFocus(): Boolean = false
+
+        @ExperimentalKeyInput
+        override fun sendKeyEvent(keyEvent: KeyEvent): Boolean = false
+
+        override fun pauseModelReadObserveration(block: () -> Unit) {
+            block()
+        }
+
+        override fun observeLayoutModelReads(node: LayoutNode, block: () -> Unit) {
+            block()
+        }
+
+        override fun observeMeasureModelReads(node: LayoutNode, block: () -> Unit) {
+            block()
+        }
+
+        override fun <T : OwnerScope> observeReads(
+            target: T,
+            onChanged: (T) -> Unit,
+            block: () -> Unit
+        ) {
+            block()
+        }
+
+        override fun measureAndLayout() {
+        }
+
+        override fun createLayer(
+            drawLayerModifier: DrawLayerModifier,
+            drawBlock: (Canvas) -> Unit,
+            invalidateParentLayer: () -> Unit
+        ): OwnedLayer {
+            return object : OwnedLayer {
+                override val layerId: Long
+                    get() = 0
+                @Suppress("UNUSED_PARAMETER")
+                override var modifier: DrawLayerModifier
+                    get() = drawLayerModifier
+                    set(value) {}
+
+                override fun updateLayerProperties() {
+                }
+
+                override fun move(position: IntOffset) {
+                }
+
+                override fun resize(size: IntSize) {
+                }
+
+                override fun drawLayer(canvas: Canvas) {
+                    drawBlock(canvas)
+                }
+
+                override fun updateDisplayList() {
+                }
+
+                override fun invalidate() {
+                }
+
+                override fun destroy() {
+                }
+
+                override fun getMatrix(matrix: Matrix) {
+                }
+
+                override val isValid: Boolean
+                    get() = true
+            }
+        }
+
+        override fun onSemanticsChange() {
+        }
+
+        override val measureIteration: Long = 0
+        override val viewConfiguration: ViewConfiguration
+            get() = TestViewConfiguration()
+    }
+
+    @OptIn(ExperimentalComposeApi::class)
+    class EmptyApplier : Applier<Unit> {
+        override val current: Unit = Unit
+        override fun down(node: Unit) {}
+        override fun up() {}
+        override fun insert(index: Int, instance: Unit) {
+            error("Unexpected")
+        }
+        override fun remove(index: Int, count: Int) {
+            error("Unexpected")
+        }
+        override fun move(from: Int, to: Int, count: Int) {
+            error("Unexpected")
+        }
+        override fun clear() {}
+    }
+
+    private class TestViewConfiguration : ViewConfiguration {
+        override val longPressTimeout: Duration
+            get() = 500.milliseconds
+
+        override val doubleTapTimeout: Duration
+            get() = 300.milliseconds
+
+        override val doubleTapMinTime: Duration
+            get() = 40.milliseconds
+
+        override val touchSlop: Float
+            get() = 18f
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
new file mode 100644
index 0000000..a54da5c9
--- /dev/null
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2019 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:OptIn(ExperimentalPointerInput::class)
+
+package androidx.compose.foundation.gestures
+
+import androidx.compose.ui.gesture.DoubleTapTimeout
+import androidx.compose.ui.gesture.ExperimentalPointerInput
+import androidx.compose.ui.gesture.LongPressTimeout
+import androidx.compose.ui.input.pointer.consumeDownChange
+import androidx.compose.ui.input.pointer.consumePositionChange
+import androidx.compose.ui.unit.inMilliseconds
+import androidx.compose.ui.unit.milliseconds
+import kotlinx.coroutines.delay
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+@OptIn(ExperimentalPointerInput::class)
+class TapGestureDetectorTest {
+    private var pressed = false
+    private var released = false
+    private var canceled = false
+    private var tapped = false
+    private var doubleTapped = false
+    private var longPressed = false
+
+    private val util = SuspendingGestureTestUtil {
+        tapGestureDetector(
+            >
+                pressed = true
+                if (tryAwaitRelease()) {
+                    released = true
+                } else {
+                    canceled = true
+                }
+            },
+            >
+                tapped = true
+            }
+        )
+    }
+
+    private val allGestures = SuspendingGestureTestUtil {
+        tapGestureDetector(
+            >
+                pressed = true
+                try {
+                    awaitRelease()
+                    released = true
+                } catch (_: GestureCancellationException) {
+                    canceled = true
+                }
+            },
+             tapped = true },
+             longPressed = true },
+             doubleTapped = true }
+        )
+    }
+
+    @Before
+    fun setup() {
+        pressed = false
+        released = false
+        canceled = false
+        tapped = false
+        doubleTapped = false
+        longPressed = false
+    }
+
+    /**
+     * Clicking in the region should result in the callback being invoked.
+     */
+    @Test
+    fun normalTap() = util.executeInComposition {
+        val down = down(5f, 5f)
+        assertTrue(down.consumed.downChange)
+
+        assertTrue(pressed)
+        assertFalse(tapped)
+        assertFalse(released)
+
+        val up = down.up(50.milliseconds)
+        assertTrue(up.consumed.downChange)
+
+        assertTrue(tapped)
+        assertTrue(released)
+        assertFalse(canceled)
+    }
+
+    /**
+     * Clicking in the region should result in the callback being invoked.
+     */
+    @Test
+    fun normalTapWithAllGestures() = allGestures.executeInComposition {
+        val down = down(5f, 5f)
+        assertTrue(down.consumed.downChange)
+
+        assertTrue(pressed)
+
+        val up = down.up(50.milliseconds)
+        assertTrue(up.consumed.downChange)
+
+        assertTrue(released)
+
+        // we have to wait for the double-tap timeout before we receive an event
+
+        assertFalse(tapped)
+        assertFalse(doubleTapped)
+
+        delay(DoubleTapTimeout.inMilliseconds() + 10)
+
+        assertTrue(tapped)
+        assertFalse(doubleTapped)
+    }
+
+    /**
+     * Clicking in the region should result in the callback being invoked.
+     */
+    @Test
+    fun normalDoubleTap() = allGestures.executeInComposition {
+        val up = down(5f, 5f)
+            .up()
+        assertTrue(up.consumed.downChange)
+
+        assertTrue(pressed)
+        assertTrue(released)
+        assertFalse(tapped)
+        assertFalse(doubleTapped)
+
+        pressed = false
+        released = false
+
+        val up2 = down(5f, 5f, 50.milliseconds)
+            .up()
+        assertTrue(up2.consumed.downChange)
+
+        assertFalse(tapped)
+        assertTrue(doubleTapped)
+        assertTrue(pressed)
+        assertTrue(released)
+    }
+
+    /**
+     * Long press in the region should result in the callback being invoked.
+     */
+    @Test
+    fun normalLongPress() = allGestures.executeInComposition {
+        val down = down(5f, 5f)
+        assertTrue(down.consumed.downChange)
+
+        assertTrue(pressed)
+        delay(LongPressTimeout.inMilliseconds() + 10)
+
+        assertTrue(longPressed)
+
+        val up = down.up(500.milliseconds)
+        assertTrue(up.consumed.downChange)
+
+        assertFalse(tapped)
+        assertFalse(doubleTapped)
+        assertTrue(released)
+        assertFalse(canceled)
+    }
+
+    /**
+     * Pressing in the region, sliding out and then lifting should result in
+     * the callback not being invoked
+     */
+    @Test
+    fun tapMiss() = util.executeInComposition {
+        val up = down(5f, 5f)
+            .moveTo(15f, 15f)
+            .up()
+
+        assertTrue(pressed)
+        assertTrue(canceled)
+        assertFalse(released)
+        assertFalse(tapped)
+        assertFalse(up.consumed.downChange)
+    }
+
+    /**
+     * Pressing in the region, sliding out and then lifting should result in
+     * the callback not being invoked
+     */
+    @Test
+    fun longPressMiss() = allGestures.executeInComposition {
+        val pointer = down(5f, 5f)
+            .moveTo(15f, 15f)
+
+        delay(DoubleTapTimeout.inMilliseconds() + 10)
+        val up = pointer.up()
+        assertFalse(up.consumed.downChange)
+
+        assertTrue(pressed)
+        assertFalse(released)
+        assertTrue(canceled)
+        assertFalse(tapped)
+        assertFalse(longPressed)
+        assertFalse(doubleTapped)
+    }
+
+    /**
+     * Pressing in the region, sliding out and then lifting should result in
+     * the callback not being invoked for double-tap
+     */
+    @Test
+    fun doubleTapMiss() = allGestures.executeInComposition {
+        val up1 = down(5f, 5f).up()
+        assertTrue(up1.consumed.downChange)
+
+        assertTrue(pressed)
+        assertTrue(released)
+        assertFalse(canceled)
+
+        pressed = false
+        released = false
+
+        val up2 = down(5f, 5f, 50.milliseconds)
+            .moveTo(15f, 15f)
+            .up()
+
+        assertFalse(up2.consumed.downChange)
+
+        assertTrue(pressed)
+        assertFalse(released)
+        assertTrue(canceled)
+        assertTrue(tapped)
+        assertFalse(longPressed)
+        assertFalse(doubleTapped)
+    }
+
+    /**
+     * Pressing in the region, sliding out, then back in, then lifting
+     * should result the gesture being canceled.
+     */
+    @Test
+    fun tapOutAndIn() = util.executeInComposition {
+        val up = down(5f, 5f)
+            .moveTo(15f, 15f)
+            .moveTo(6f, 6f)
+            .up()
+
+        assertFalse(tapped)
+        assertFalse(up.consumed.downChange)
+        assertTrue(pressed)
+        assertFalse(released)
+        assertTrue(canceled)
+    }
+
+    /**
+     * After a first tap, a second tap should also be detected.
+     */
+    @Test
+    fun secondTap() = util.executeInComposition {
+        down(5f, 5f)
+            .up()
+
+        assertTrue(pressed)
+        assertTrue(released)
+        assertFalse(canceled)
+
+        tapped = false
+        pressed = false
+        released = false
+
+        val up2 = down(4f, 4f)
+            .up()
+        assertTrue(tapped)
+        assertTrue(up2.consumed.downChange)
+        assertTrue(pressed)
+        assertTrue(released)
+        assertFalse(canceled)
+    }
+
+    /**
+     * Clicking in the region with the up already consumed should result in the callback not
+     * being invoked.
+     */
+    @Test
+    fun consumedUpTap() = util.executeInComposition {
+        val down = down(5f, 5f)
+
+        assertFalse(tapped)
+        assertTrue(pressed)
+
+        down.up {
+            consumeDownChange()
+        }
+
+        assertFalse(tapped)
+        assertFalse(released)
+        assertTrue(canceled)
+    }
+
+    /**
+     * Clicking in the region with the motion consumed should result in the callback not
+     * being invoked.
+     */
+    @Test
+    fun consumedMotionTap() = util.executeInComposition {
+        down(5f, 5f)
+            .moveTo(6f, 2f) {
+                consumePositionChange(1f, -3f)
+            }
+            .up(50.milliseconds)
+
+        assertFalse(tapped)
+        assertTrue(pressed)
+        assertFalse(released)
+        assertTrue(canceled)
+    }
+
+    /**
+     * Ensure that two-finger taps work.
+     */
+    @Test
+    fun twoFingerTap() = util.executeInComposition {
+        val down = down(1f, 1f)
+        assertTrue(down.consumed.downChange)
+
+        assertTrue(pressed)
+        pressed = false
+
+        val down2 = down(9f, 5f)
+        assertFalse(down2.consumed.downChange)
+
+        assertFalse(pressed)
+
+        val up = down.up()
+        assertFalse(up.consumed.downChange)
+        assertFalse(tapped)
+        assertFalse(released)
+
+        val up2 = down2.up()
+        assertTrue(up2.consumed.downChange)
+
+        assertTrue(tapped)
+        assertTrue(released)
+        assertFalse(canceled)
+    }
+
+    /**
+     * A position change consumption on any finger should cause tap to cancel.
+     */
+    @Test
+    fun twoFingerTapCancel() = util.executeInComposition {
+        val down = down(1f, 1f)
+
+        assertTrue(pressed)
+
+        val down2 = down(9f, 5f)
+
+        val up = down.moveTo(5f, 5f) {
+            consumePositionChange(4f, 4f)
+        }.up()
+        assertFalse(up.consumed.downChange)
+
+        assertFalse(tapped)
+        assertTrue(canceled)
+
+        val up2 = down2.up(50.milliseconds)
+        assertFalse(up2.consumed.downChange)
+
+        assertFalse(tapped)
+        assertFalse(released)
+    }
+
+    /**
+     * Detect the second tap as long press.
+     */
+    @Test
+    fun secondTapLongPress() = allGestures.executeInComposition {
+        down(5f, 5f).up()
+
+        assertTrue(pressed)
+        assertTrue(released)
+        assertFalse(canceled)
+        assertFalse(tapped)
+        assertFalse(doubleTapped)
+        assertFalse(longPressed)
+
+        pressed = false
+        released = false
+
+        val secondDown = down(5f, 5f, 50.milliseconds)
+
+        assertTrue(pressed)
+
+        delay(LongPressTimeout.inMilliseconds() + 10)
+
+        assertTrue(tapped)
+        assertTrue(longPressed)
+        assertFalse(released)
+        assertFalse(canceled)
+
+        secondDown.up(500.milliseconds)
+        assertTrue(released)
+    }
+}
\ No newline at end of file
diff --git a/compose/integration-tests/benchmark/build.gradle b/compose/integration-tests/benchmark/build.gradle
index f46802f..f9efbbfe2 100644
--- a/compose/integration-tests/benchmark/build.gradle
+++ b/compose/integration-tests/benchmark/build.gradle
@@ -33,6 +33,7 @@
     kotlinPlugin project(":compose:compiler:compiler")
 
     implementation project(":benchmark:benchmark-junit4")
+    implementation project(":benchmark:benchmark-perfetto")
     implementation project(":compose:foundation:foundation-layout")
     implementation project(":compose:integration-tests")
     implementation project(":compose:runtime:runtime")
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmark.kt
index 64f3096..bfe8f59 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmark.kt
@@ -35,7 +35,7 @@
  */
 @LargeTest
 @RunWith(AndroidJUnit4::class)
-class VectorBenchmark {
+open class VectorBenchmark {
     @get:Rule
     val benchmarkRule = ComposeBenchmarkRule()
 
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmarkWithTracing.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmarkWithTracing.kt
new file mode 100644
index 0000000..d4e1688
--- /dev/null
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmarkWithTracing.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.ui.benchmark.test
+
+import androidx.benchmark.perfetto.PerfettoRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.runner.RunWith
+
+/**
+ * Duplicate of [VectorBenchmark], but which adds tracing.
+ *
+ * Note: Per PerfettoRule, these benchmarks will be ignored < API 29
+ */
+@Suppress("ClassName")
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class VectorBenchmarkWithTracing : VectorBenchmark() {
+    @get:Rule
+    val perfettoRule = PerfettoRule()
+}
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
index 3fbb8ac..0542dd6 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
@@ -117,7 +117,7 @@
 ) {
     MaterialTheme(demoColors.colors) {
         val statusBarColor = with(MaterialTheme.colors) {
-            if (isLight) darkenedPrimary else surface.toArgb()
+            (if (isLight) primaryVariant else Color.Black).toArgb()
         }
         onCommit(statusBarColor) {
             window.statusBarColor = statusBarColor
@@ -126,15 +126,6 @@
     }
 }
 
-private val Colors.darkenedPrimary: Int
-    get() = with(primary) {
-        copy(
-            red = red * 0.75f,
-            green = green * 0.75f,
-            blue = blue * 0.75f
-        )
-    }.toArgb()
-
 private class Navigator private constructor(
     private val backDispatcher: OnBackPressedDispatcher,
     private val launchActivityDemo: (ActivityDemo<*>) -> Unit,
diff --git a/compose/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt b/compose/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
index 1050403..a5803b3 100644
--- a/compose/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
+++ b/compose/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
@@ -28,7 +28,6 @@
         return listOf(
             ModifierInspectorInfoDetector.ISSUE,
             UnnecessaryLambdaCreationDetector.ISSUE,
-            PackageNameMigrationDetector.ISSUE
         ) + AndroidXIssueRegistry.Issues
     }
 }
diff --git a/compose/internal-lint-checks/src/main/java/androidx/compose/lint/ModifierInspectorInfoDetector.kt b/compose/internal-lint-checks/src/main/java/androidx/compose/lint/ModifierInspectorInfoDetector.kt
index 89ef5a3..f8b50a0 100644
--- a/compose/internal-lint-checks/src/main/java/androidx/compose/lint/ModifierInspectorInfoDetector.kt
+++ b/compose/internal-lint-checks/src/main/java/androidx/compose/lint/ModifierInspectorInfoDetector.kt
@@ -657,7 +657,7 @@
     companion object {
         val ISSUE = Issue.create(
             id = "ModifierInspectorInfo",
-            briefDescription = "Modifiers should include inspectorInfo for the Layout Inspector",
+            briefDescription = "Modifier missing inspectorInfo",
             explanation =
                 """
                 The Layout Inspector will see an instance of the usually private modifier class \
diff --git a/compose/internal-lint-checks/src/main/java/androidx/compose/lint/PackageNameMigrationDetector.kt b/compose/internal-lint-checks/src/main/java/androidx/compose/lint/PackageNameMigrationDetector.kt
deleted file mode 100644
index 769c3ca..0000000
--- a/compose/internal-lint-checks/src/main/java/androidx/compose/lint/PackageNameMigrationDetector.kt
+++ /dev/null
@@ -1,104 +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("UnstableApiUsage")
-
-package androidx.compose.lint
-
-import com.android.tools.lint.client.api.UElementHandler
-import com.android.tools.lint.detector.api.Category
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Implementation
-import com.android.tools.lint.detector.api.Issue
-import com.android.tools.lint.detector.api.JavaContext
-import com.android.tools.lint.detector.api.Scope
-import com.android.tools.lint.detector.api.Severity
-import com.android.tools.lint.detector.api.SourceCodeScanner
-import org.jetbrains.uast.UFile
-
-/**
- * Simple lint check that prevents using old package names (map defined in
- * [PackageNameMigrationMap]) after a library has migrated to the new name.
- *
- * TODO: b/160233169 remove this lint check after the migration has finished.
- */
-class PackageNameMigrationDetector : Detector(), SourceCodeScanner {
-    override fun getApplicableUastTypes() = listOf(UFile::class.java)
-
-    override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
-        override fun visitFile(node: UFile) {
-            val packageName = node.packageName
-
-            PackageNameMigrationMap.keys.find { packageName.contains(it) }?.let {
-                val newPackageName = PackageNameMigrationMap[it]
-                context.report(
-                    ISSUE,
-                    node,
-                    context.getLocation(node),
-                    "The package name '$packageName' has been migrated to '$newPackageName', " +
-                        "please update the package name of this file accordingly."
-                )
-            }
-        }
-    }
-
-    companion object {
-        private val PackageNameMigrationMap: Map<String, String> = mapOf(
-            // placeholder package name used in PackageNameMigrationDetectorTest, since the
-            // migration has not started yet
-            "androidx.ui.foo" to "androidx.compose.foo",
-            "androidx.ui.livedata" to "androidx.compose.runtime.livedata",
-            "androidx.ui.rxjava2" to "androidx.compose.runtime.rxjava2",
-            "androidx.ui.savedinstancestate" to "androidx.compose.runtime.savedinstancestate",
-            "androidx.ui.foundation" to "androidx.compose.foundation",
-            "androidx.ui.layout" to "androidx.compose.foundation.layout",
-            "androidx.animation" to "androidx.compose.animation.core",
-            "androidx.ui.animation" to "androidx.compose.animation",
-            "androidx.compose.dispatch" to "androidx.compose.runtime.dispatch",
-            "androidx.ui.text" to "androidx.compose.ui.text",
-            "androidx.ui.input" to "androidx.compose.ui.text.input",
-            "androidx.ui.intl" to "androidx.compose.ui.text.intl",
-            "androidx.ui.geometry" to "androidx.compose.ui.geometry",
-            "androidx.ui.graphics" to "androidx.compose.ui.graphics",
-            "androidx.ui.unit" to "androidx.compose.ui.unit",
-            "androidx.ui.util" to "androidx.compose.ui.util",
-            "androidx.ui.material" to "androidx.compose.material",
-            "androidx.compose.plugins" to "androidx.compose.compiler.plugins",
-            "androidx.ui.autofill" to "androidx.compose.ui.autofill",
-            "androidx.ui.res" to "androidx.compose.ui.res",
-            "androidx.ui.platform" to "androidx.compose.ui.platform",
-            "androidx.ui.semantics" to "androidx.compose.ui.semantics",
-            "androidx.ui.testutils" to "no replacement package",
-            "androidx.ui.viewinterop" to "androidx.compose.ui.viewinterop",
-            "androidx.ui.viewmodel" to "androidx.compose.ui.viewinterop",
-            "androidx.ui.core" to "androidx.compose.ui"
-        )
-
-        val ISSUE = Issue.create(
-            "PackageNameMigration",
-            "Using an old package name that has recently been migrated to androidx.compose",
-            "As part of a large migration from androidx.ui to androidx.compose, package names " +
-                "across all libraries are being refactored. If you are seeing this Lint " +
-                "error, you are adding new files to the old package name, once the rest of " +
-                "the library has migrated to the new package name.",
-            Category.PERFORMANCE, 5, Severity.ERROR,
-            Implementation(
-                PackageNameMigrationDetector::class.java,
-                Scope.JAVA_FILE_SCOPE
-            )
-        )
-    }
-}
diff --git a/compose/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt b/compose/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
index ca0f454..60ca677 100644
--- a/compose/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
+++ b/compose/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
@@ -25,16 +25,18 @@
 import com.android.tools.lint.detector.api.Scope
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiElement
 import com.intellij.psi.impl.source.PsiClassReferenceType
 import org.jetbrains.kotlin.psi.KtCallExpression
 import org.jetbrains.kotlin.psi.KtCallableDeclaration
 import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
 import org.jetbrains.uast.ULambdaExpression
-import org.jetbrains.uast.UReferenceExpression
 import org.jetbrains.uast.kotlin.KotlinUBlockExpression
 import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
 import org.jetbrains.uast.kotlin.KotlinUImplicitReturnExpression
 import org.jetbrains.uast.resolveToUElement
+import org.jetbrains.uast.toUElement
+import org.jetbrains.uast.tryResolve
 
 /**
  * Lint [Detector] to ensure that we are not creating extra lambdas just to emit already captured
@@ -142,10 +144,13 @@
             val expectedComposable =
                 parentDeclaration.valueParameters[parameterIndex]!!.isComposable
 
-            val receiver = (expression.receiver as? UReferenceExpression)
-                ?.resolveToUElement()?.sourcePsi as? KtCallableDeclaration ?: return
+            // Hack to get the psi of the lambda declaration / source. The !!s here probably
+            // aren't safe, but nothing fails with them currently - so it could be a useful
+            // indicator if something breaks in the future to let us know to update this lint check.
+            val resolvedLambda = expression.sourcePsi.calleeExpression!!.toUElement()!!.tryResolve()
+                .toUElement()!!.sourcePsi!!
 
-            val isComposable = receiver.isComposable
+            val isComposable = resolvedLambda.isComposable
 
             if (isComposable != expectedComposable) return
 
@@ -185,9 +190,9 @@
 }
 
 /**
- * @return whether this [KtCallableDeclaration] is annotated with @Composable
+ * @return whether this [PsiElement] contains @Composable in its source
  */
-private val KtCallableDeclaration.isComposable: Boolean
+private val PsiElement.isComposable: Boolean
     // Unfortunately as Composability isn't carried through UAST, and there are many types of
     // declarations (types such as foo: @Composable () -> Unit, properties such as val
     // foo = @Composable {}) the best way to cover this is just check if we contain this annotation
diff --git a/compose/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt b/compose/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt
index f4d7440..906edb9 100644
--- a/compose/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt
+++ b/compose/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt
@@ -645,7 +645,7 @@
             .run()
             .expect(
                 """
-                    src/androidx/compose/ui/SizeModifier2.kt:8: Error: Modifiers should include inspectorInfo for the Layout Inspector [ModifierInspectorInfo]
+                    src/androidx/compose/ui/SizeModifier2.kt:8: Error: Modifier missing inspectorInfo [ModifierInspectorInfo]
                     fun Modifier.preferredWidth2(width: Int) = this.then(SizeModifier2(width))
                                                                          ~~~~~~~~~~~~~
                     1 errors, 0 warnings
@@ -679,7 +679,7 @@
             .run()
             .expect(
                 """
-                    src/androidx/compose/ui/BorderModifier.kt:9: Error: Modifiers should include inspectorInfo for the Layout Inspector [ModifierInspectorInfo]
+                    src/androidx/compose/ui/BorderModifier.kt:9: Error: Modifier missing inspectorInfo [ModifierInspectorInfo]
                         composed { this.then(BorderModifier(width)) }
                         ~~~~~~~~
                     1 errors, 0 warnings
@@ -718,7 +718,7 @@
             .run()
             .expect(
                 """
-                    src/androidx/compose/ui/SizeModifier.kt:12: Error: Modifiers should include inspectorInfo for the Layout Inspector [ModifierInspectorInfo]
+                    src/androidx/compose/ui/SizeModifier.kt:12: Error: Modifier missing inspectorInfo [ModifierInspectorInfo]
                         this.then(SizeModifier.WithOption(width))
                                                ~~~~~~~~~~
                     1 errors, 0 warnings
diff --git a/compose/internal-lint-checks/src/test/java/androidx/compose/lint/PackageNameMigrationDetectorTest.kt b/compose/internal-lint-checks/src/test/java/androidx/compose/lint/PackageNameMigrationDetectorTest.kt
deleted file mode 100644
index 36967b4..0000000
--- a/compose/internal-lint-checks/src/test/java/androidx/compose/lint/PackageNameMigrationDetectorTest.kt
+++ /dev/null
@@ -1,77 +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("UnstableApiUsage")
-
-package androidx.compose.lint
-
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Issue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-/* ktlint-disable max-line-length */
-@RunWith(JUnit4::class)
-/**
- * Test for [PackageNameMigrationDetector]
- *
- * TODO: b/160233169 remove this lint check after the migration has finished.
- */
-class PackageNameMigrationDetectorTest : LintDetectorTest() {
-    override fun getDetector(): Detector = PackageNameMigrationDetector()
-
-    override fun getIssues(): MutableList<Issue> = mutableListOf(PackageNameMigrationDetector.ISSUE)
-
-    @Test
-    fun oldPackageShouldFail() {
-        lint().files(
-            kotlin(
-                """
-                package androidx.ui.foo
-
-                fun someApi() {}
-            """
-            )
-        )
-            .run()
-            .expect(
-                """
-src/androidx/ui/foo/test.kt:1: Error: The package name 'androidx.ui.foo' has been migrated to 'androidx.compose.foo', please update the package name of this file accordingly. [PackageNameMigration]
-
-^
-1 errors, 0 warnings
-            """
-            )
-    }
-
-    @Test
-    fun newPackageShouldPass() {
-        lint().files(
-            kotlin(
-                """
-                package androidx.compose.foo
-
-                fun someApi() {}
-            """
-            )
-        )
-            .run()
-            .expectClean()
-    }
-}
-/* ktlint-enable max-line-length */
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt
index bbd9e40..cce4023 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt
@@ -45,7 +45,6 @@
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.layout.id
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.Constraints
@@ -247,9 +246,9 @@
             ) { label() }
         }
     ) { measurables, constraints ->
-        val iconPlaceable = measurables.first { it.id == "icon" }.measure(constraints)
+        val iconPlaceable = measurables.first { it.layoutId == "icon" }.measure(constraints)
 
-        val labelPlaceable = measurables.first { it.id == "label" }.measure(
+        val labelPlaceable = measurables.first { it.layoutId == "label" }.measure(
             // Measure with loose constraints for height as we don't want the label to take up more
             // space than it needs
             constraints.copy(minHeight = 0)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
index e8e04f7..46dade7 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
@@ -37,7 +37,6 @@
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.layout.id
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.SoftwareKeyboardController
@@ -380,13 +379,13 @@
         // measure leading icon
         val constraints =
             incomingConstraints.copy(minWidth = 0, minHeight = 0)
-        val leadingPlaceable = measurables.find { it.id == "leading" }?.measure(constraints)
+        val leadingPlaceable = measurables.find { it.layoutId == "leading" }?.measure(constraints)
         occupiedSpaceHorizontally += widthOrZero(
             leadingPlaceable
         )
 
         // measure trailing icon
-        val trailingPlaceable = measurables.find { it.id == "trailing" }
+        val trailingPlaceable = measurables.find { it.layoutId == "trailing" }
             ?.measure(constraints.offset(horizontal = -occupiedSpaceHorizontally))
         occupiedSpaceHorizontally += widthOrZero(
             trailingPlaceable
@@ -398,7 +397,7 @@
             vertical = -bottomPadding
         )
         val labelPlaceable =
-            measurables.find { it.id == LabelId }?.measure(labelConstraints)
+            measurables.find { it.layoutId == LabelId }?.measure(labelConstraints)
         onLabelMeasured(labelPlaceable?.width ?: 0)
 
         // measure text field
@@ -410,12 +409,12 @@
             vertical = -bottomPadding - topPadding
         ).copy(minHeight = 0)
         val textFieldPlaceable =
-            measurables.first { it.id == TextFieldId }.measure(textContraints)
+            measurables.first { it.layoutId == TextFieldId }.measure(textContraints)
 
         // measure placeholder
         val placeholderConstraints = textContraints.copy(minWidth = 0)
         val placeholderPlaceable =
-            measurables.find { it.id == PlaceholderId }?.measure(placeholderConstraints)
+            measurables.find { it.layoutId == PlaceholderId }?.measure(placeholderConstraints)
 
         val width =
             calculateWidth(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
index 6fd3ca9..3a59d60 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
@@ -32,7 +32,6 @@
 import androidx.compose.ui.layout.FirstBaseline
 import androidx.compose.ui.layout.LastBaseline
 import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.id
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
@@ -300,11 +299,11 @@
             bottom = SnackbarVerticalPadding
         )
     ) { measurables, constraints ->
-        val buttonPlaceable = measurables.first { it.id == actionTag }.measure(constraints)
+        val buttonPlaceable = measurables.first { it.layoutId == actionTag }.measure(constraints)
         val textMaxWidth =
             (constraints.maxWidth - buttonPlaceable.width - TextEndExtraSpacing.toIntPx())
                 .coerceAtLeast(constraints.minWidth)
-        val textPlaceable = measurables.first { it.id == textTag }.measure(
+        val textPlaceable = measurables.first { it.layoutId == textTag }.measure(
             constraints.copy(minHeight = 0, maxWidth = textMaxWidth)
         )
 
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
index ce168b17..069820b 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
@@ -50,7 +50,6 @@
 import androidx.compose.ui.layout.LastBaseline
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.layout.id
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.text.style.TextAlign
@@ -320,13 +319,13 @@
             Box(Modifier.layoutId("icon")) { icon() }
         }
     ) { measurables, constraints ->
-        val textPlaceable = measurables.first { it.id == "text" }.measure(
+        val textPlaceable = measurables.first { it.layoutId == "text" }.measure(
             // Measure with loose constraints for height as we don't want the text to take up more
             // space than it needs
             constraints.copy(minHeight = 0)
         )
 
-        val iconPlaceable = measurables.first { it.id == "icon" }.measure(constraints)
+        val iconPlaceable = measurables.first { it.layoutId == "icon" }.measure(constraints)
 
         val hasTextPlaceable =
             textPlaceable.width != 0 && textPlaceable.height != 0
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
index ab6a97f..a8dc5ca 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
@@ -38,7 +38,6 @@
 import androidx.compose.ui.layout.LastBaseline
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.layout.id
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.SoftwareKeyboardController
@@ -408,13 +407,13 @@
         // measure leading icon
         val constraints = incomingConstraints.copy(minWidth = 0, minHeight = 0)
         val leadingPlaceable =
-            measurables.find { it.id == "leading" }?.measure(constraints)
+            measurables.find { it.layoutId == "leading" }?.measure(constraints)
         occupiedSpaceHorizontally += widthOrZero(
             leadingPlaceable
         )
 
         // measure trailing icon
-        val trailingPlaceable = measurables.find { it.id == "trailing" }
+        val trailingPlaceable = measurables.find { it.layoutId == "trailing" }
             ?.measure(constraints.offset(horizontal = -occupiedSpaceHorizontally))
         occupiedSpaceHorizontally += widthOrZero(
             trailingPlaceable
@@ -427,7 +426,7 @@
                 horizontal = -occupiedSpaceHorizontally
             )
         val labelPlaceable =
-            measurables.find { it.id == LabelId }?.measure(labelConstraints)
+            measurables.find { it.layoutId == LabelId }?.measure(labelConstraints)
         val lastBaseline = labelPlaceable?.get(LastBaseline)?.let {
             if (it != AlignmentLine.Unspecified) it else labelPlaceable.height
         } ?: 0
@@ -441,13 +440,13 @@
                 horizontal = -occupiedSpaceHorizontally
             )
         val textFieldPlaceable = measurables
-            .first { it.id == TextFieldId }
+            .first { it.layoutId == TextFieldId }
             .measure(textFieldConstraints)
 
         // measure placeholder
         val placeholderConstraints = textFieldConstraints.copy(minWidth = 0)
         val placeholderPlaceable = measurables
-            .find { it.id == PlaceholderId }
+            .find { it.layoutId == PlaceholderId }
             ?.measure(placeholderConstraints)
 
         val width = calculateWidth(
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
index a9366ac..563fabe0 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
@@ -28,5 +28,5 @@
      * IMPORTANT: Whenever updating this value, please make sure to also update `versionTable` and
      * `minimumRuntimeVersionInt` in `VersionChecker.kt` of the compiler.
      */
-    const val version: Int = 1800
+    const val version: Int = 1900
 }
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 0276f50..4b655b0 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -1049,7 +1049,7 @@
 package androidx.compose.ui.text.platform {
 
   public final class AndroidAccessibilitySpannableStringKt {
-    method @androidx.compose.ui.text.InternalTextApi public static android.text.SpannableString toAccessibilitySpannableString(androidx.compose.ui.text.AnnotatedString);
+    method @androidx.compose.ui.text.InternalTextApi public static android.text.SpannableString toAccessibilitySpannableString(androidx.compose.ui.text.AnnotatedString, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.Font.ResourceLoader resourceLoader);
   }
 
   public final class AndroidParagraphHelperKt {
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index 0276f50..4b655b0 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -1049,7 +1049,7 @@
 package androidx.compose.ui.text.platform {
 
   public final class AndroidAccessibilitySpannableStringKt {
-    method @androidx.compose.ui.text.InternalTextApi public static android.text.SpannableString toAccessibilitySpannableString(androidx.compose.ui.text.AnnotatedString);
+    method @androidx.compose.ui.text.InternalTextApi public static android.text.SpannableString toAccessibilitySpannableString(androidx.compose.ui.text.AnnotatedString, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.Font.ResourceLoader resourceLoader);
   }
 
   public final class AndroidParagraphHelperKt {
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 0276f50..4b655b0 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -1049,7 +1049,7 @@
 package androidx.compose.ui.text.platform {
 
   public final class AndroidAccessibilitySpannableStringKt {
-    method @androidx.compose.ui.text.InternalTextApi public static android.text.SpannableString toAccessibilitySpannableString(androidx.compose.ui.text.AnnotatedString);
+    method @androidx.compose.ui.text.InternalTextApi public static android.text.SpannableString toAccessibilitySpannableString(androidx.compose.ui.text.AnnotatedString, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.Font.ResourceLoader resourceLoader);
   }
 
   public final class AndroidParagraphHelperKt {
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
index 80724fa..a88a311 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
@@ -16,15 +16,37 @@
 
 package androidx.compose.ui.text.platform
 
+import android.graphics.Typeface
 import android.text.SpannableString
+import android.text.style.AbsoluteSizeSpan
+import android.text.style.BackgroundColorSpan
+import android.text.style.ForegroundColorSpan
 import android.text.style.LocaleSpan
+import android.text.style.RelativeSizeSpan
+import android.text.style.ScaleXSpan
+import android.text.style.StrikethroughSpan
+import android.text.style.StyleSpan
+import android.text.style.TypefaceSpan
+import android.text.style.UnderlineSpan
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TestFontResourceLoader
 import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.intl.LocaleList
+import androidx.compose.ui.text.matchers.assertThat
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextGeometricTransform
 import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
 import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
+import androidx.test.platform.app.InstrumentationRegistry
 import org.junit.Test
 import org.junit.runner.RunWith
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -33,20 +55,233 @@
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class AndroidAccessibilitySpannableStringTest {
+    private val context = InstrumentationRegistry.getInstrumentation().context
+    private val density = Density(context)
+    private val resourceLoader = TestFontResourceLoader(context)
+
     @Test
-    fun toAccessibilitySpannableString_with_localeSpan() {
+    fun toAccessibilitySpannableString_with_locale() {
+        val languageTag = "en-GB"
         val annotatedString = annotatedString {
             append("hello")
-            withStyle(style = SpanStyle(localeList = LocaleList("en-gb"))) {
+            withStyle(style = SpanStyle(localeList = LocaleList(languageTag))) {
                 append("world")
             }
         }
 
-        val spannableString = annotatedString.toAccessibilitySpannableString()
-        assertThat(spannableString).isInstanceOf(SpannableString::class.java)
+        val spannableString =
+            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
 
-        assertThat(
-            spannableString.getSpans(0, spannableString.length, LocaleSpan::class.java)
-        ).isNotEmpty()
+        assertThat(spannableString).isInstanceOf(SpannableString::class.java)
+        assertThat(spannableString).hasSpan(
+            LocaleSpan::class, 5, 10
+        ) {
+            it.locale?.language == languageTag
+            true
+        }
+    }
+
+    @Test
+    fun toAccessibilitySpannableString_with_color() {
+        val color = Color.Black
+        val annotatedString = annotatedString {
+            append("hello")
+            withStyle(style = SpanStyle(color = color)) {
+                append("world")
+            }
+        }
+
+        val spannableString =
+            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+
+        assertThat(spannableString).isInstanceOf(SpannableString::class.java)
+        assertThat(spannableString).hasSpan(
+            ForegroundColorSpan::class, 5, 10
+        ) {
+            it.foregroundColor == color.toArgb()
+        }
+    }
+
+    @Test
+    fun toAccessibilitySpannableString_with_fontSizeInSp() {
+        val fontSize = 12.sp
+        val annotatedString = annotatedString {
+            append("hello")
+            withStyle(style = SpanStyle(fontSize = fontSize)) {
+                append("world")
+            }
+        }
+
+        val spannableString =
+            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+
+        assertThat(spannableString).isInstanceOf(SpannableString::class.java)
+        assertThat(spannableString).hasSpan(
+            AbsoluteSizeSpan::class, 5, 10
+        ) {
+            it.size == with(density) { fontSize.toIntPx() }
+        }
+    }
+
+    @Test
+    fun toAccessibilitySpannableString_with_fontSizeInEm() {
+        val fontSize = 2.em
+        val annotatedString = annotatedString {
+            append("hello")
+            withStyle(style = SpanStyle(fontSize = fontSize)) {
+                append("world")
+            }
+        }
+
+        val spannableString =
+            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+
+        assertThat(spannableString).isInstanceOf(SpannableString::class.java)
+        assertThat(spannableString).hasSpan(
+            RelativeSizeSpan::class, 5, 10
+        ) {
+            it.sizeChange == fontSize.value
+        }
+    }
+
+    @Test
+    fun toAccessibilitySpannableString_with_fontWeightBold() {
+        val annotatedString = annotatedString {
+            append("hello")
+            withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
+                append("world")
+            }
+        }
+
+        val spannableString =
+            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+
+        assertThat(spannableString).isInstanceOf(SpannableString::class.java)
+        assertThat(spannableString).hasSpan(
+            StyleSpan::class, 5, 10
+        ) {
+            it.style == Typeface.BOLD
+        }
+    }
+
+    @Test
+    fun toAccessibilitySpannableString_with_italic() {
+        val annotatedString = annotatedString {
+            append("hello")
+            withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) {
+                append("world")
+            }
+        }
+
+        val spannableString =
+            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+
+        assertThat(spannableString).isInstanceOf(SpannableString::class.java)
+        assertThat(spannableString).hasSpan(
+            StyleSpan::class, 5, 10
+        ) {
+            it.style == Typeface.ITALIC
+        }
+    }
+
+    @Test
+    fun toAccessibilitySpannableString_with_fontFamily() {
+        val fontFamily = FontFamily.Monospace
+        val annotatedString = annotatedString {
+            append("hello")
+            withStyle(style = SpanStyle(fontFamily = fontFamily)) {
+                append("world")
+            }
+        }
+
+        val spannableString =
+            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+
+        assertThat(spannableString).isInstanceOf(SpannableString::class.java)
+        assertThat(spannableString).hasSpan(
+            TypefaceSpan::class, 5, 10
+        ) {
+            it.family == "monospace"
+        }
+    }
+
+    @Test
+    fun toAccessibilitySpannableString_with_underline() {
+        val annotatedString = annotatedString {
+            append("hello")
+            withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) {
+                append("world")
+            }
+        }
+
+        val spannableString =
+            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+
+        assertThat(spannableString).isInstanceOf(SpannableString::class.java)
+        assertThat(spannableString).hasSpan(
+            UnderlineSpan::class, 5, 10
+        )
+    }
+
+    @Test
+    fun toAccessibilitySpannableString_with_lineThrough() {
+        val annotatedString = annotatedString {
+            append("hello")
+            withStyle(style = SpanStyle(textDecoration = TextDecoration.LineThrough)) {
+                append("world")
+            }
+        }
+
+        val spannableString =
+            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+
+        assertThat(spannableString).isInstanceOf(SpannableString::class.java)
+        assertThat(spannableString).hasSpan(
+            StrikethroughSpan::class, 5, 10
+        )
+    }
+
+    @Test
+    fun toAccessibilitySpannableString_with_scaleX() {
+        val scaleX = 1.2f
+        val annotatedString = annotatedString {
+            append("hello")
+            withStyle(
+                style = SpanStyle(textGeometricTransform = TextGeometricTransform(scaleX = scaleX))
+            ) {
+                append("world")
+            }
+        }
+
+        val spannableString =
+            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+
+        assertThat(spannableString).isInstanceOf(SpannableString::class.java)
+        assertThat(spannableString).hasSpan(
+            ScaleXSpan::class, 5, 10
+        ) {
+            it.scaleX == scaleX
+        }
+    }
+
+    @Test
+    fun toAccessibilitySpannableString_with_background() {
+        val backgroundColor = Color.Red
+        val annotatedString = annotatedString {
+            append("hello")
+            withStyle(style = SpanStyle(background = backgroundColor)) {
+                append("world")
+            }
+        }
+
+        val spannableString =
+            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+
+        assertThat(spannableString).isInstanceOf(SpannableString::class.java)
+        assertThat(spannableString).hasSpan(
+            BackgroundColorSpan::class, 5, 10
+        ) {
+            it.backgroundColor == backgroundColor.toArgb()
+        }
     }
 }
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.kt
index cf4f540..9d79b22 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.kt
@@ -16,21 +16,120 @@
 
 package androidx.compose.ui.text.platform
 
+import android.graphics.Typeface
+import android.os.Build
 import android.text.SpannableString
+import android.text.Spanned
+import android.text.style.ScaleXSpan
+import android.text.style.StyleSpan
+import android.text.style.TypefaceSpan
+import androidx.annotation.RequiresApi
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontSynthesis
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.font.GenericFontFamily
+import androidx.compose.ui.text.platform.TypefaceAdapter.Companion.getTypefaceStyle
+import androidx.compose.ui.text.platform.extensions.setBackground
+import androidx.compose.ui.text.platform.extensions.setColor
+import androidx.compose.ui.text.platform.extensions.setFontSize
 import androidx.compose.ui.text.platform.extensions.setLocaleList
+import androidx.compose.ui.text.platform.extensions.setTextDecoration
+import androidx.compose.ui.unit.Density
 import androidx.compose.ui.util.fastForEach
 
 /**
  * Convert an AnnotatedString into SpannableString for Android text to speech support.
  */
 @InternalTextApi
-fun AnnotatedString.toAccessibilitySpannableString(): SpannableString {
+fun AnnotatedString.toAccessibilitySpannableString(
+    density: Density,
+    resourceLoader: Font.ResourceLoader
+): SpannableString {
     val spannableString = SpannableString(text)
+    val typefaceAdapter = TypefaceAdapter(resourceLoader = resourceLoader)
     spanStyles.fastForEach { (style, start, end) ->
-        spannableString.setLocaleList(style.localeList, start, end)
+        spannableString.setSpanStyle(style, start, end, density, typefaceAdapter)
+    }
+    return spannableString
+}
+
+/** Apply the serializable styles to SpannableString. */
+private fun SpannableString.setSpanStyle(
+    spanStyle: SpanStyle,
+    start: Int,
+    end: Int,
+    density: Density,
+    typefaceAdapter: TypefaceAdapter
+) {
+    setColor(spanStyle.color, start, end)
+
+    setFontSize(spanStyle.fontSize, density, start, end)
+
+    if (spanStyle.fontWeight != null || spanStyle.fontStyle != null) {
+        // If current typeface is bold, StyleSpan won't change it to normal. The same applies to
+        // font style, so use normal as default works here.
+        // This is also a bug in framework span. But we can't find a good solution so far.
+        val fontWeight = spanStyle.fontWeight ?: FontWeight.Normal
+        val fontStyle = spanStyle.fontStyle ?: FontStyle.Normal
+        setSpan(
+            StyleSpan(getTypefaceStyle(fontWeight, fontStyle)),
+            start,
+            end,
+            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+        )
     }
 
-    return spannableString
+    // TypefaceSpan accepts Typeface as parameter only after P. And only font family string can be
+    // pass to other thread.
+    // Here we try to create TypefaceSpan with font family string if possible.
+    if (spanStyle.fontFamily != null) {
+        if (spanStyle.fontFamily is GenericFontFamily) {
+            setSpan(
+                TypefaceSpan(spanStyle.fontFamily.name),
+                start,
+                end,
+                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+            )
+        } else {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                val typeface = typefaceAdapter.create(
+                    fontFamily = spanStyle.fontFamily,
+                    fontSynthesis = spanStyle.fontSynthesis ?: FontSynthesis.All
+                )
+                setSpan(
+                    Api28Impl.createTypefaceSpan(typeface),
+                    start,
+                    end,
+                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+                )
+            }
+        }
+    }
+
+    setTextDecoration(spanStyle.textDecoration, start, end)
+
+    if (
+        spanStyle.textGeometricTransform != null &&
+        spanStyle.textGeometricTransform.scaleX != 1f
+    ) {
+        setSpan(
+            ScaleXSpan(spanStyle.textGeometricTransform.scaleX),
+            start,
+            end,
+            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+        )
+    }
+
+    setLocaleList(spanStyle.localeList, start, end)
+
+    setBackground(spanStyle.background, start, end)
+}
+
+@RequiresApi(28)
+private object Api28Impl {
+    fun createTypefaceSpan(typeface: Typeface): TypefaceSpan = TypefaceSpan(typeface)
 }
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.kt
index 16d7f45..cfdb309 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.kt
@@ -263,7 +263,7 @@
     }
 }
 
-private fun Spannable.setBackground(color: Color, start: Int, end: Int) {
+internal fun Spannable.setBackground(color: Color, start: Int, end: Int) {
     if (color.isSpecified) {
         setSpan(
             BackgroundColorSpan(color.toArgb()),
@@ -329,7 +329,7 @@
     }
 }
 
-private fun Spannable.setFontSize(fontSize: TextUnit, density: Density, start: Int, end: Int) {
+internal fun Spannable.setFontSize(fontSize: TextUnit, density: Density, start: Int, end: Int) {
     when (fontSize.type) {
         TextUnitType.Sp -> with(density) {
             setSpan(
@@ -346,7 +346,7 @@
     }
 }
 
-private fun Spannable.setTextDecoration(textDecoration: TextDecoration?, start: Int, end: Int) {
+internal fun Spannable.setTextDecoration(textDecoration: TextDecoration?, start: Int, end: Int) {
     textDecoration?.let {
         if (TextDecoration.Underline in it) {
             setSpan(UnderlineSpan(), start, end)
@@ -357,7 +357,7 @@
     }
 }
 
-private fun Spannable.setColor(color: Color, start: Int, end: Int) {
+internal fun Spannable.setColor(color: Color, start: Int, end: Int) {
     if (color.isSpecified) {
         setSpan(ForegroundColorSpan(color.toArgb()), start, end)
     }
diff --git a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
index 2f4ca13..5009fad 100644
--- a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
+++ b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
@@ -16,11 +16,16 @@
 
 package androidx.compose.ui.util
 
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+
 /**
  * Iterates through a [List] using the index and calls [action] for each item.
  * This does not allocate an iterator like [Iterable.forEach].
  */
+@OptIn(ExperimentalContracts::class)
 inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
+    contract { callsInPlace(action) }
     for (index in indices) {
         val item = get(index)
         action(item)
@@ -31,7 +36,9 @@
  * Iterates through a [List] using the index and calls [action] for each item.
  * This does not allocate an iterator like [Iterable.forEachIndexed].
  */
+@OptIn(ExperimentalContracts::class)
 inline fun <T> List<T>.fastForEachIndexed(action: (Int, T) -> Unit) {
+    contract { callsInPlace(action) }
     for (index in indices) {
         val item = get(index)
         action(index, item)
@@ -41,7 +48,9 @@
 /**
  * Returns `true` if all elements match the given [predicate].
  */
+@OptIn(ExperimentalContracts::class)
 inline fun <T> List<T>.fastAll(predicate: (T) -> Boolean): Boolean {
+    contract { callsInPlace(predicate) }
     fastForEach { if (!predicate(it)) return false }
     return true
 }
@@ -49,7 +58,9 @@
 /**
  * Returns `true` if at least one element matches the given [predicate].
  */
+@OptIn(ExperimentalContracts::class)
 inline fun <T> List<T>.fastAny(predicate: (T) -> Boolean): Boolean {
+    contract { callsInPlace(predicate) }
     fastForEach { if (predicate(it)) return true }
     return false
 }
@@ -57,7 +68,9 @@
 /**
  * Returns the first value that [predicate] returns `true` for or `null` if nothing matches.
  */
+@OptIn(ExperimentalContracts::class)
 inline fun <T> List<T>.fastFirstOrNull(predicate: (T) -> Boolean): T? {
+    contract { callsInPlace(predicate) }
     fastForEach { if (predicate(it)) return it }
     return null
 }
@@ -66,7 +79,9 @@
  * Returns the sum of all values produced by [selector] function applied to each element in the
  * list.
  */
+@OptIn(ExperimentalContracts::class)
 inline fun <T> List<T>.fastSumBy(selector: (T) -> Int): Int {
+    contract { callsInPlace(selector) }
     var sum = 0
     fastForEach { element ->
         sum += selector(element)
@@ -78,7 +93,9 @@
  * Returns a list containing the results of applying the given [transform] function
  * to each element in the original collection.
  */
+@OptIn(ExperimentalContracts::class)
 inline fun <T, R> List<T>.fastMap(transform: (T) -> R): List<R> {
+    contract { callsInPlace(transform) }
     val target = ArrayList<R>(size)
     fastForEach {
         target += transform(it)
@@ -90,7 +107,9 @@
  * Returns the first element yielding the largest value of the given function or `null` if there
  * are no elements.
  */
+@OptIn(ExperimentalContracts::class)
 inline fun <T, R : Comparable<R>> List<T>.fastMaxBy(selector: (T) -> R): T? {
+    contract { callsInPlace(selector) }
     if (isEmpty()) return null
     var maxElem = get(0)
     var maxValue = selector(maxElem)
@@ -109,10 +128,12 @@
  * Applies the given [transform] function to each element of the original collection
  * and appends the results to the given [destination].
  */
+@OptIn(ExperimentalContracts::class)
 inline fun <T, R, C : MutableCollection<in R>> List<T>.fastMapTo(
     destination: C,
     transform: (T) -> R
 ): C {
+    contract { callsInPlace(transform) }
     fastForEach { item ->
         destination.add(transform(item))
     }
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 62005ed..5779a75 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1428,6 +1428,10 @@
   @androidx.compose.ui.gesture.ExperimentalPointerInput @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope {
     method public suspend Object? awaitCustomEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.CustomEvent> p);
     method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
+    method public java.util.List<androidx.compose.ui.input.pointer.PointerInputData> getCurrentPointers();
+    method public long getSize-YbymL2g();
+    property public abstract java.util.List<androidx.compose.ui.input.pointer.PointerInputData> currentPointers;
+    property public abstract long size;
   }
 
   public final class HitPathTrackerKt {
@@ -1457,6 +1461,7 @@
     method public static void consumeAllChanges(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static void consumeDownChange(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static void consumePositionChange(androidx.compose.ui.input.pointer.PointerInputChange, float consumedDx, float consumedDy);
+    method public static boolean isOutOfBounds-MReStF0(androidx.compose.ui.input.pointer.PointerInputChange, long size);
     method public static long positionChange(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static long positionChangeIgnoreConsumed(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static boolean positionChanged(androidx.compose.ui.input.pointer.PointerInputChange);
@@ -1549,9 +1554,11 @@
   @androidx.compose.ui.gesture.ExperimentalPointerInput public interface PointerInputScope extends androidx.compose.ui.unit.Density {
     method public androidx.compose.ui.input.pointer.CustomEventDispatcher getCustomEventDispatcher();
     method public long getSize-YbymL2g();
+    method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
     method public suspend <R> Object? handlePointerInput(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.HandlePointerInputScope,? super kotlin.coroutines.Continuation<? super R>,?> handler, kotlin.coroutines.Continuation<? super R> p);
     property public abstract androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher;
     property public abstract long size;
+    property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
 
   public final class PointerInputTestUtilKt {
@@ -1591,18 +1598,21 @@
   }
 
   @androidx.compose.runtime.Stable public interface ContentScale {
-    method public float scale-AhF4CD4(long srcSize, long dstSize);
+    method public long computeScaleFactor-AhF4CD4(long srcSize, long dstSize);
+    method @Deprecated public default float scale-AhF4CD4(long srcSize, long dstSize);
     field public static final androidx.compose.ui.layout.ContentScale.Companion Companion;
   }
 
   public static final class ContentScale.Companion {
     method public androidx.compose.ui.layout.ContentScale getCrop();
+    method public androidx.compose.ui.layout.ContentScale getFillBounds();
     method public androidx.compose.ui.layout.ContentScale getFillHeight();
     method public androidx.compose.ui.layout.ContentScale getFillWidth();
     method public androidx.compose.ui.layout.ContentScale getFit();
     method public androidx.compose.ui.layout.ContentScale getInside();
     method public androidx.compose.ui.layout.FixedScale getNone();
     property public final androidx.compose.ui.layout.ContentScale Crop;
+    property public final androidx.compose.ui.layout.ContentScale FillBounds;
     property public final androidx.compose.ui.layout.ContentScale FillHeight;
     property public final androidx.compose.ui.layout.ContentScale FillWidth;
     property public final androidx.compose.ui.layout.ContentScale Fit;
@@ -1616,9 +1626,9 @@
   @androidx.compose.runtime.Immutable public final class FixedScale implements androidx.compose.ui.layout.ContentScale {
     ctor public FixedScale(float value);
     method public float component1();
+    method public long computeScaleFactor-AhF4CD4(long srcSize, long dstSize);
     method @androidx.compose.runtime.Immutable public androidx.compose.ui.layout.FixedScale copy(float value);
     method public float getValue();
-    method public float scale-AhF4CD4(long srcSize, long dstSize);
     property public final float value;
   }
 
@@ -1670,13 +1680,14 @@
   }
 
   public final class LayoutIdKt {
-    method public static Object? getId(androidx.compose.ui.layout.Measurable);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier layoutId(androidx.compose.ui.Modifier, Object id);
+    method @Deprecated public static Object! getId(androidx.compose.ui.layout.Measurable);
+    method public static Object? getLayoutId(androidx.compose.ui.layout.Measurable);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier layoutId(androidx.compose.ui.Modifier, Object layoutId);
   }
 
   public interface LayoutIdParentData {
-    method public Object getId();
-    property public abstract Object id;
+    method public Object getLayoutId();
+    property public abstract Object layoutId;
   }
 
   public final class LayoutKt {
@@ -1800,6 +1811,36 @@
     method public void onRemeasurementAvailable(androidx.compose.ui.layout.Remeasurement remeasurement);
   }
 
+  @androidx.compose.runtime.Immutable public final inline class ScaleFactor {
+    ctor public ScaleFactor();
+    method @androidx.compose.runtime.Stable public static inline operator float component1-impl(long $this);
+    method @androidx.compose.runtime.Stable public static inline operator float component2-impl(long $this);
+    method public static long constructor-impl(internal long packedValue);
+    method public static long copy-_hLwfpc(long $this, optional float scaleX, optional float scaleY);
+    method @androidx.compose.runtime.Stable public static operator long div-_hLwfpc(long $this, float operand);
+    method @androidx.compose.runtime.Immutable public static inline boolean equals-impl(long p, Object? p1);
+    method public static boolean equals-impl0(long p1, long p2);
+    method public static float getScaleX-impl(long $this);
+    method public static float getScaleY-impl(long $this);
+    method @androidx.compose.runtime.Immutable public static inline int hashCode-impl(long p);
+    method @androidx.compose.runtime.Stable public static operator long times-_hLwfpc(long $this, float operand);
+    method public static String toString-impl(long $this);
+    field public static final androidx.compose.ui.layout.ScaleFactor.Companion Companion;
+  }
+
+  public static final class ScaleFactor.Companion {
+    method public long getUnspecified-_hLwfpc();
+    property public final long Unspecified;
+  }
+
+  public final class ScaleFactorKt {
+    method @androidx.compose.runtime.Stable public static long ScaleFactor(float scaleX, float scaleY);
+    method @androidx.compose.runtime.Stable public static operator long div-ngKnWWw(long, long scaleFactor);
+    method @androidx.compose.runtime.Stable public static long lerp-bKVCie4(long start, long stop, float fraction);
+    method @androidx.compose.runtime.Stable public static operator long times-Sp6zcS4(long, long size);
+    method @androidx.compose.runtime.Stable public static operator long times-ngKnWWw(long, long scaleFactor);
+  }
+
   public final class SubcomposeLayoutKt {
     method @androidx.compose.runtime.Composable public static <T> void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope<T>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
   }
@@ -1964,6 +2005,7 @@
     method public boolean getShowLayoutBounds();
     method public androidx.compose.ui.text.input.TextInputService getTextInputService();
     method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
+    method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
     method public void measureAndLayout();
     method public void observeLayoutModelReads(androidx.compose.ui.node.LayoutNode node, kotlin.jvm.functions.Function0<kotlin.Unit> block);
     method public void observeMeasureModelReads(androidx.compose.ui.node.LayoutNode node, kotlin.jvm.functions.Function0<kotlin.Unit> block);
@@ -1991,6 +2033,7 @@
     property public abstract boolean showLayoutBounds;
     property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
     property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
+    property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
     field public static final androidx.compose.ui.node.Owner.Companion Companion;
   }
 
@@ -2056,6 +2099,7 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.text.input.TextInputService> getTextInputServiceAmbient();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.TextToolbar> getTextToolbarAmbient();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.UriHandler> getUriHandlerAmbient();
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getViewConfigurationAmbient();
   }
 
   public final class AndroidAmbientsKt {
@@ -2102,6 +2146,18 @@
     method public void openUri(String uri);
   }
 
+  public final class AndroidViewConfiguration implements androidx.compose.ui.platform.ViewConfiguration {
+    ctor public AndroidViewConfiguration(android.view.ViewConfiguration viewConfiguration);
+    method public long getDoubleTapMinTime-ojFfpTE();
+    method public long getDoubleTapTimeout-ojFfpTE();
+    method public long getLongPressTimeout-ojFfpTE();
+    method public float getTouchSlop();
+    property public long doubleTapMinTime;
+    property public long doubleTapTimeout;
+    property public long longPressTimeout;
+    property public float touchSlop;
+  }
+
   public interface ClipboardManager {
     method public androidx.compose.ui.text.AnnotatedString? getText();
     method public void setText(androidx.compose.ui.text.AnnotatedString annotatedString);
@@ -2207,6 +2263,17 @@
     method public operator void set(String name, Object? value);
   }
 
+  public interface ViewConfiguration {
+    method public long getDoubleTapMinTime-ojFfpTE();
+    method public long getDoubleTapTimeout-ojFfpTE();
+    method public long getLongPressTimeout-ojFfpTE();
+    method public float getTouchSlop();
+    property public abstract long doubleTapMinTime;
+    property public abstract long doubleTapTimeout;
+    property public abstract long longPressTimeout;
+    property public abstract float touchSlop;
+  }
+
   public final class WrapperKt {
     method public static androidx.compose.runtime.Composition setContent(androidx.activity.ComponentActivity, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method public static androidx.compose.runtime.Composition setContent(android.view.ViewGroup, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 62005ed..5779a75 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -1428,6 +1428,10 @@
   @androidx.compose.ui.gesture.ExperimentalPointerInput @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope {
     method public suspend Object? awaitCustomEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.CustomEvent> p);
     method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
+    method public java.util.List<androidx.compose.ui.input.pointer.PointerInputData> getCurrentPointers();
+    method public long getSize-YbymL2g();
+    property public abstract java.util.List<androidx.compose.ui.input.pointer.PointerInputData> currentPointers;
+    property public abstract long size;
   }
 
   public final class HitPathTrackerKt {
@@ -1457,6 +1461,7 @@
     method public static void consumeAllChanges(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static void consumeDownChange(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static void consumePositionChange(androidx.compose.ui.input.pointer.PointerInputChange, float consumedDx, float consumedDy);
+    method public static boolean isOutOfBounds-MReStF0(androidx.compose.ui.input.pointer.PointerInputChange, long size);
     method public static long positionChange(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static long positionChangeIgnoreConsumed(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static boolean positionChanged(androidx.compose.ui.input.pointer.PointerInputChange);
@@ -1549,9 +1554,11 @@
   @androidx.compose.ui.gesture.ExperimentalPointerInput public interface PointerInputScope extends androidx.compose.ui.unit.Density {
     method public androidx.compose.ui.input.pointer.CustomEventDispatcher getCustomEventDispatcher();
     method public long getSize-YbymL2g();
+    method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
     method public suspend <R> Object? handlePointerInput(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.HandlePointerInputScope,? super kotlin.coroutines.Continuation<? super R>,?> handler, kotlin.coroutines.Continuation<? super R> p);
     property public abstract androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher;
     property public abstract long size;
+    property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
 
   public final class PointerInputTestUtilKt {
@@ -1591,18 +1598,21 @@
   }
 
   @androidx.compose.runtime.Stable public interface ContentScale {
-    method public float scale-AhF4CD4(long srcSize, long dstSize);
+    method public long computeScaleFactor-AhF4CD4(long srcSize, long dstSize);
+    method @Deprecated public default float scale-AhF4CD4(long srcSize, long dstSize);
     field public static final androidx.compose.ui.layout.ContentScale.Companion Companion;
   }
 
   public static final class ContentScale.Companion {
     method public androidx.compose.ui.layout.ContentScale getCrop();
+    method public androidx.compose.ui.layout.ContentScale getFillBounds();
     method public androidx.compose.ui.layout.ContentScale getFillHeight();
     method public androidx.compose.ui.layout.ContentScale getFillWidth();
     method public androidx.compose.ui.layout.ContentScale getFit();
     method public androidx.compose.ui.layout.ContentScale getInside();
     method public androidx.compose.ui.layout.FixedScale getNone();
     property public final androidx.compose.ui.layout.ContentScale Crop;
+    property public final androidx.compose.ui.layout.ContentScale FillBounds;
     property public final androidx.compose.ui.layout.ContentScale FillHeight;
     property public final androidx.compose.ui.layout.ContentScale FillWidth;
     property public final androidx.compose.ui.layout.ContentScale Fit;
@@ -1616,9 +1626,9 @@
   @androidx.compose.runtime.Immutable public final class FixedScale implements androidx.compose.ui.layout.ContentScale {
     ctor public FixedScale(float value);
     method public float component1();
+    method public long computeScaleFactor-AhF4CD4(long srcSize, long dstSize);
     method @androidx.compose.runtime.Immutable public androidx.compose.ui.layout.FixedScale copy(float value);
     method public float getValue();
-    method public float scale-AhF4CD4(long srcSize, long dstSize);
     property public final float value;
   }
 
@@ -1670,13 +1680,14 @@
   }
 
   public final class LayoutIdKt {
-    method public static Object? getId(androidx.compose.ui.layout.Measurable);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier layoutId(androidx.compose.ui.Modifier, Object id);
+    method @Deprecated public static Object! getId(androidx.compose.ui.layout.Measurable);
+    method public static Object? getLayoutId(androidx.compose.ui.layout.Measurable);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier layoutId(androidx.compose.ui.Modifier, Object layoutId);
   }
 
   public interface LayoutIdParentData {
-    method public Object getId();
-    property public abstract Object id;
+    method public Object getLayoutId();
+    property public abstract Object layoutId;
   }
 
   public final class LayoutKt {
@@ -1800,6 +1811,36 @@
     method public void onRemeasurementAvailable(androidx.compose.ui.layout.Remeasurement remeasurement);
   }
 
+  @androidx.compose.runtime.Immutable public final inline class ScaleFactor {
+    ctor public ScaleFactor();
+    method @androidx.compose.runtime.Stable public static inline operator float component1-impl(long $this);
+    method @androidx.compose.runtime.Stable public static inline operator float component2-impl(long $this);
+    method public static long constructor-impl(internal long packedValue);
+    method public static long copy-_hLwfpc(long $this, optional float scaleX, optional float scaleY);
+    method @androidx.compose.runtime.Stable public static operator long div-_hLwfpc(long $this, float operand);
+    method @androidx.compose.runtime.Immutable public static inline boolean equals-impl(long p, Object? p1);
+    method public static boolean equals-impl0(long p1, long p2);
+    method public static float getScaleX-impl(long $this);
+    method public static float getScaleY-impl(long $this);
+    method @androidx.compose.runtime.Immutable public static inline int hashCode-impl(long p);
+    method @androidx.compose.runtime.Stable public static operator long times-_hLwfpc(long $this, float operand);
+    method public static String toString-impl(long $this);
+    field public static final androidx.compose.ui.layout.ScaleFactor.Companion Companion;
+  }
+
+  public static final class ScaleFactor.Companion {
+    method public long getUnspecified-_hLwfpc();
+    property public final long Unspecified;
+  }
+
+  public final class ScaleFactorKt {
+    method @androidx.compose.runtime.Stable public static long ScaleFactor(float scaleX, float scaleY);
+    method @androidx.compose.runtime.Stable public static operator long div-ngKnWWw(long, long scaleFactor);
+    method @androidx.compose.runtime.Stable public static long lerp-bKVCie4(long start, long stop, float fraction);
+    method @androidx.compose.runtime.Stable public static operator long times-Sp6zcS4(long, long size);
+    method @androidx.compose.runtime.Stable public static operator long times-ngKnWWw(long, long scaleFactor);
+  }
+
   public final class SubcomposeLayoutKt {
     method @androidx.compose.runtime.Composable public static <T> void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope<T>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
   }
@@ -1964,6 +2005,7 @@
     method public boolean getShowLayoutBounds();
     method public androidx.compose.ui.text.input.TextInputService getTextInputService();
     method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
+    method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
     method public void measureAndLayout();
     method public void observeLayoutModelReads(androidx.compose.ui.node.LayoutNode node, kotlin.jvm.functions.Function0<kotlin.Unit> block);
     method public void observeMeasureModelReads(androidx.compose.ui.node.LayoutNode node, kotlin.jvm.functions.Function0<kotlin.Unit> block);
@@ -1991,6 +2033,7 @@
     property public abstract boolean showLayoutBounds;
     property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
     property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
+    property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
     field public static final androidx.compose.ui.node.Owner.Companion Companion;
   }
 
@@ -2056,6 +2099,7 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.text.input.TextInputService> getTextInputServiceAmbient();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.TextToolbar> getTextToolbarAmbient();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.UriHandler> getUriHandlerAmbient();
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getViewConfigurationAmbient();
   }
 
   public final class AndroidAmbientsKt {
@@ -2102,6 +2146,18 @@
     method public void openUri(String uri);
   }
 
+  public final class AndroidViewConfiguration implements androidx.compose.ui.platform.ViewConfiguration {
+    ctor public AndroidViewConfiguration(android.view.ViewConfiguration viewConfiguration);
+    method public long getDoubleTapMinTime-ojFfpTE();
+    method public long getDoubleTapTimeout-ojFfpTE();
+    method public long getLongPressTimeout-ojFfpTE();
+    method public float getTouchSlop();
+    property public long doubleTapMinTime;
+    property public long doubleTapTimeout;
+    property public long longPressTimeout;
+    property public float touchSlop;
+  }
+
   public interface ClipboardManager {
     method public androidx.compose.ui.text.AnnotatedString? getText();
     method public void setText(androidx.compose.ui.text.AnnotatedString annotatedString);
@@ -2207,6 +2263,17 @@
     method public operator void set(String name, Object? value);
   }
 
+  public interface ViewConfiguration {
+    method public long getDoubleTapMinTime-ojFfpTE();
+    method public long getDoubleTapTimeout-ojFfpTE();
+    method public long getLongPressTimeout-ojFfpTE();
+    method public float getTouchSlop();
+    property public abstract long doubleTapMinTime;
+    property public abstract long doubleTapTimeout;
+    property public abstract long longPressTimeout;
+    property public abstract float touchSlop;
+  }
+
   public final class WrapperKt {
     method public static androidx.compose.runtime.Composition setContent(androidx.activity.ComponentActivity, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method public static androidx.compose.runtime.Composition setContent(android.view.ViewGroup, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index eee4efa..1cf3e36 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1428,6 +1428,10 @@
   @androidx.compose.ui.gesture.ExperimentalPointerInput @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope {
     method public suspend Object? awaitCustomEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.CustomEvent> p);
     method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
+    method public java.util.List<androidx.compose.ui.input.pointer.PointerInputData> getCurrentPointers();
+    method public long getSize-YbymL2g();
+    property public abstract java.util.List<androidx.compose.ui.input.pointer.PointerInputData> currentPointers;
+    property public abstract long size;
   }
 
   public final class HitPathTrackerKt {
@@ -1457,6 +1461,7 @@
     method public static void consumeAllChanges(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static void consumeDownChange(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static void consumePositionChange(androidx.compose.ui.input.pointer.PointerInputChange, float consumedDx, float consumedDy);
+    method public static boolean isOutOfBounds-MReStF0(androidx.compose.ui.input.pointer.PointerInputChange, long size);
     method public static long positionChange(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static long positionChangeIgnoreConsumed(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static boolean positionChanged(androidx.compose.ui.input.pointer.PointerInputChange);
@@ -1549,9 +1554,11 @@
   @androidx.compose.ui.gesture.ExperimentalPointerInput public interface PointerInputScope extends androidx.compose.ui.unit.Density {
     method public androidx.compose.ui.input.pointer.CustomEventDispatcher getCustomEventDispatcher();
     method public long getSize-YbymL2g();
+    method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
     method public suspend <R> Object? handlePointerInput(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.HandlePointerInputScope,? super kotlin.coroutines.Continuation<? super R>,?> handler, kotlin.coroutines.Continuation<? super R> p);
     property public abstract androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher;
     property public abstract long size;
+    property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
 
   public final class PointerInputTestUtilKt {
@@ -1591,18 +1598,21 @@
   }
 
   @androidx.compose.runtime.Stable public interface ContentScale {
-    method public float scale-AhF4CD4(long srcSize, long dstSize);
+    method public long computeScaleFactor-AhF4CD4(long srcSize, long dstSize);
+    method @Deprecated public default float scale-AhF4CD4(long srcSize, long dstSize);
     field public static final androidx.compose.ui.layout.ContentScale.Companion Companion;
   }
 
   public static final class ContentScale.Companion {
     method public androidx.compose.ui.layout.ContentScale getCrop();
+    method public androidx.compose.ui.layout.ContentScale getFillBounds();
     method public androidx.compose.ui.layout.ContentScale getFillHeight();
     method public androidx.compose.ui.layout.ContentScale getFillWidth();
     method public androidx.compose.ui.layout.ContentScale getFit();
     method public androidx.compose.ui.layout.ContentScale getInside();
     method public androidx.compose.ui.layout.FixedScale getNone();
     property public final androidx.compose.ui.layout.ContentScale Crop;
+    property public final androidx.compose.ui.layout.ContentScale FillBounds;
     property public final androidx.compose.ui.layout.ContentScale FillHeight;
     property public final androidx.compose.ui.layout.ContentScale FillWidth;
     property public final androidx.compose.ui.layout.ContentScale Fit;
@@ -1633,9 +1643,9 @@
   @androidx.compose.runtime.Immutable public final class FixedScale implements androidx.compose.ui.layout.ContentScale {
     ctor public FixedScale(float value);
     method public float component1();
+    method public long computeScaleFactor-AhF4CD4(long srcSize, long dstSize);
     method @androidx.compose.runtime.Immutable public androidx.compose.ui.layout.FixedScale copy(float value);
     method public float getValue();
-    method public float scale-AhF4CD4(long srcSize, long dstSize);
     property public final float value;
   }
 
@@ -1716,13 +1726,14 @@
   }
 
   public final class LayoutIdKt {
-    method public static Object? getId(androidx.compose.ui.layout.Measurable);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier layoutId(androidx.compose.ui.Modifier, Object id);
+    method @Deprecated public static Object! getId(androidx.compose.ui.layout.Measurable);
+    method public static Object? getLayoutId(androidx.compose.ui.layout.Measurable);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier layoutId(androidx.compose.ui.Modifier, Object layoutId);
   }
 
   public interface LayoutIdParentData {
-    method public Object getId();
-    property public abstract Object id;
+    method public Object getLayoutId();
+    property public abstract Object layoutId;
   }
 
   public final class LayoutKt {
@@ -1847,6 +1858,36 @@
     method public void onRemeasurementAvailable(androidx.compose.ui.layout.Remeasurement remeasurement);
   }
 
+  @androidx.compose.runtime.Immutable public final inline class ScaleFactor {
+    ctor public ScaleFactor();
+    method @androidx.compose.runtime.Stable public static inline operator float component1-impl(long $this);
+    method @androidx.compose.runtime.Stable public static inline operator float component2-impl(long $this);
+    method public static long constructor-impl(internal long packedValue);
+    method public static long copy-_hLwfpc(long $this, optional float scaleX, optional float scaleY);
+    method @androidx.compose.runtime.Stable public static operator long div-_hLwfpc(long $this, float operand);
+    method @androidx.compose.runtime.Immutable public static inline boolean equals-impl(long p, Object? p1);
+    method public static boolean equals-impl0(long p1, long p2);
+    method public static float getScaleX-impl(long $this);
+    method public static float getScaleY-impl(long $this);
+    method @androidx.compose.runtime.Immutable public static inline int hashCode-impl(long p);
+    method @androidx.compose.runtime.Stable public static operator long times-_hLwfpc(long $this, float operand);
+    method public static String toString-impl(long $this);
+    field public static final androidx.compose.ui.layout.ScaleFactor.Companion Companion;
+  }
+
+  public static final class ScaleFactor.Companion {
+    method public long getUnspecified-_hLwfpc();
+    property public final long Unspecified;
+  }
+
+  public final class ScaleFactorKt {
+    method @androidx.compose.runtime.Stable public static long ScaleFactor(float scaleX, float scaleY);
+    method @androidx.compose.runtime.Stable public static operator long div-ngKnWWw(long, long scaleFactor);
+    method @androidx.compose.runtime.Stable public static long lerp-bKVCie4(long start, long stop, float fraction);
+    method @androidx.compose.runtime.Stable public static operator long times-Sp6zcS4(long, long size);
+    method @androidx.compose.runtime.Stable public static operator long times-ngKnWWw(long, long scaleFactor);
+  }
+
   public final class SubcomposeLayoutKt {
     method @androidx.compose.runtime.Composable public static <T> void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope<T>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
   }
@@ -2026,6 +2067,7 @@
     method public boolean getShowLayoutBounds();
     method public androidx.compose.ui.text.input.TextInputService getTextInputService();
     method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
+    method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
     method public void measureAndLayout();
     method public void observeLayoutModelReads(androidx.compose.ui.node.LayoutNode node, kotlin.jvm.functions.Function0<kotlin.Unit> block);
     method public void observeMeasureModelReads(androidx.compose.ui.node.LayoutNode node, kotlin.jvm.functions.Function0<kotlin.Unit> block);
@@ -2053,6 +2095,7 @@
     property public abstract boolean showLayoutBounds;
     property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
     property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
+    property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
     field public static final androidx.compose.ui.node.Owner.Companion Companion;
   }
 
@@ -2127,6 +2170,7 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.text.input.TextInputService> getTextInputServiceAmbient();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.TextToolbar> getTextToolbarAmbient();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.UriHandler> getUriHandlerAmbient();
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getViewConfigurationAmbient();
   }
 
   public final class AndroidAmbientsKt {
@@ -2179,6 +2223,18 @@
     method public void openUri(String uri);
   }
 
+  public final class AndroidViewConfiguration implements androidx.compose.ui.platform.ViewConfiguration {
+    ctor public AndroidViewConfiguration(android.view.ViewConfiguration viewConfiguration);
+    method public long getDoubleTapMinTime-ojFfpTE();
+    method public long getDoubleTapTimeout-ojFfpTE();
+    method public long getLongPressTimeout-ojFfpTE();
+    method public float getTouchSlop();
+    property public long doubleTapMinTime;
+    property public long doubleTapTimeout;
+    property public long longPressTimeout;
+    property public float touchSlop;
+  }
+
   public interface ClipboardManager {
     method public androidx.compose.ui.text.AnnotatedString? getText();
     method public void setText(androidx.compose.ui.text.AnnotatedString annotatedString);
@@ -2284,6 +2340,17 @@
     method public operator void set(String name, Object? value);
   }
 
+  public interface ViewConfiguration {
+    method public long getDoubleTapMinTime-ojFfpTE();
+    method public long getDoubleTapTimeout-ojFfpTE();
+    method public long getLongPressTimeout-ojFfpTE();
+    method public float getTouchSlop();
+    property public abstract long doubleTapMinTime;
+    property public abstract long doubleTapTimeout;
+    property public abstract long longPressTimeout;
+    property public abstract float touchSlop;
+  }
+
   public final class WrapperKt {
     method public static androidx.compose.runtime.Composition setContent(androidx.activity.ComponentActivity, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method public static androidx.compose.runtime.Composition setContent(android.view.ViewGroup, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
diff --git a/compose/ui/ui/integration-tests/ui-demos/lint-baseline.xml b/compose/ui/ui/integration-tests/ui-demos/lint-baseline.xml
index ab8f3898..d20265e 100644
--- a/compose/ui/ui/integration-tests/ui-demos/lint-baseline.xml
+++ b/compose/ui/ui/integration-tests/ui-demos/lint-baseline.xml
@@ -3,7 +3,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="    composed {"
         errorLine2="    ~~~~~~~~">
         <location
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/MultipleCollect.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/MultipleCollect.kt
index 1e6d835..0496cc5 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/MultipleCollect.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/MultipleCollect.kt
@@ -25,7 +25,6 @@
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.id
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.unit.Constraints
 
@@ -40,15 +39,15 @@
         Box(Modifier.layoutId("footer")) { footer() }
         content()
     }) { measurables, constraints ->
-        val headerPlaceable = measurables.first { it.id == "header" }.measure(
+        val headerPlaceable = measurables.first { it.layoutId == "header" }.measure(
             Constraints.fixed(constraints.maxWidth, 100)
         )
         val footerPadding = 50
-        val footerPlaceable = measurables.first { it.id == "footer" }.measure(
+        val footerPlaceable = measurables.first { it.layoutId == "footer" }.measure(
             Constraints.fixed(constraints.maxWidth - footerPadding * 2, 100)
         )
 
-        val contentMeasurables = measurables.filter { it.id == null }
+        val contentMeasurables = measurables.filter { it.layoutId == null }
         val itemHeight =
             (constraints.maxHeight - headerPlaceable.height - footerPlaceable.height) /
                 contentMeasurables.size
diff --git a/compose/ui/ui/lint-baseline.xml b/compose/ui/ui/lint-baseline.xml
index c77e050..d20c93e 100644
--- a/compose/ui/ui/lint-baseline.xml
+++ b/compose/ui/ui/lint-baseline.xml
@@ -289,40 +289,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
-        errorLine1=") = this.then(DrawBackgroundModifier(onDraw))"
-        errorLine2="              ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt"
-            line="58"
-            column="15"/>
-    </issue>
-
-    <issue
-        id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
-        errorLine1=") = composed {"
-        errorLine2="    ~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt"
-            line="86"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
-        errorLine1="): Modifier = this.then(object : DrawModifier {"
-        errorLine2="                        ^">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt"
-            line="182"
-            column="25"/>
-    </issue>
-
-    <issue
-        id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1=") = if (elevation > 0.dp || clip) {"
         errorLine2="    ^">
         <location
@@ -333,7 +300,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="    return this.then(FocusObserverModifierImpl(onFocusChange))"
         errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -344,7 +311,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="    return this.then(FocusRequesterModifierImpl(focusRequester))"
         errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -355,7 +322,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="fun Modifier.keyInputFilter(onKeyEvent: (KeyEvent) -> Boolean): Modifier = composed {"
         errorLine2="                                                                           ~~~~~~~~">
         <location
@@ -366,7 +333,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="fun Modifier.previewKeyInputFilter(onPreviewKeyEvent: (KeyEvent) -> Boolean): Modifier = composed {"
         errorLine2="                                                                                         ~~~~~~~~">
         <location
@@ -377,7 +344,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="): Modifier = composed {"
         errorLine2="              ~~~~~~~~">
         <location
@@ -388,7 +355,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="): Modifier = composed {"
         errorLine2="              ~~~~~~~~">
         <location
@@ -399,7 +366,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1=") = this.then(object : OnGloballyPositionedModifier {"
         errorLine2="              ^">
         <location
@@ -410,7 +377,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1=") = composed {"
         errorLine2="    ~~~~~~~~">
         <location
@@ -421,7 +388,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="): Modifier = composed {"
         errorLine2="              ~~~~~~~~">
         <location
@@ -432,7 +399,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="): Modifier = composed {"
         errorLine2="              ~~~~~~~~">
         <location
@@ -443,7 +410,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="): Modifier = composed {"
         errorLine2="              ~~~~~~~~">
         <location
@@ -454,7 +421,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="): Modifier = composed {"
         errorLine2="              ~~~~~~~~">
         <location
@@ -465,7 +432,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="): Modifier = composed {"
         errorLine2="              ~~~~~~~~">
         <location
@@ -476,7 +443,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="): Modifier = composed {"
         errorLine2="              ~~~~~~~~">
         <location
@@ -487,7 +454,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="): Modifier = composed {"
         errorLine2="              ~~~~~~~~">
         <location
@@ -498,7 +465,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="): Modifier = composed {"
         errorLine2="              ~~~~~~~~">
         <location
@@ -509,7 +476,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="): Modifier = composed {"
         errorLine2="              ~~~~~~~~">
         <location
@@ -520,7 +487,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1=") = composed {"
         errorLine2="    ~~~~~~~~">
         <location
@@ -531,7 +498,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="): Modifier = composed {"
         errorLine2="              ~~~~~~~~">
         <location
@@ -542,7 +509,7 @@
 
     <issue
         id="ModifierInspectorInfo"
-        message="Modifiers should include inspectorInfo for the Layout Inspector"
+        message="Modifier missing inspectorInfo"
         errorLine1="): Modifier = composed {"
         errorLine2="              ~~~~~~~~">
         <location
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayoutSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayoutSample.kt
index 9c37170..82df66f 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayoutSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayoutSample.kt
@@ -24,7 +24,6 @@
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.id
 import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.layout.measureBlocksOf
@@ -162,7 +161,7 @@
         Box(Modifier.layoutId("footer")) { footer() }
     }) { measurables, constraints ->
         val placeables = measurables.map { measurable ->
-            when (measurable.id) {
+            when (measurable.layoutId) {
                 // You should use appropriate constraints. Here we measure fake constraints.
                 "header" -> measurable.measure(Constraints.fixed(100, 100))
                 "footer" -> measurable.measure(constraints)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/ParentDataModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/ParentDataModifierTest.kt
index 3599782..218afec 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/ParentDataModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/ParentDataModifierTest.kt
@@ -126,8 +126,8 @@
                 }
 
                 Layout({ header(); footer() }) { measurables, _ ->
-                    assertEquals(0, ((measurables[0]).parentData as? LayoutIdParentData)?.id)
-                    assertEquals(1, ((measurables[1]).parentData as? LayoutIdParentData)?.id)
+                    assertEquals(0, ((measurables[0]).parentData as? LayoutIdParentData)?.layoutId)
+                    assertEquals(1, ((measurables[1]).parentData as? LayoutIdParentData)?.layoutId)
                     layout(0, 0) { }
                 }
             }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
index 7689df0..b4496f4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
@@ -24,14 +24,21 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.AtLeastSize
+import androidx.compose.ui.CacheDrawScope
+import androidx.compose.ui.DrawResult
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.drawBehind
 import androidx.compose.ui.drawWithCache
+import androidx.compose.ui.drawWithContent
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.BlendMode
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.InspectableValue
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.captureToImage
@@ -41,7 +48,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -53,6 +63,16 @@
     @get:Rule
     val rule = createComposeRule()
 
+    @Before
+    fun before() {
+        isDebugInspectorInfoEnabled = true
+    }
+
+    @After
+    fun after() {
+        isDebugInspectorInfoEnabled = false
+    }
+
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
     fun testCacheHitWithStateChange() {
@@ -344,5 +364,41 @@
         }
     }
 
+    @Test
+    fun testInspectorValueForDrawBehind() {
+        val onDraw: DrawScope.() -> Unit = {}
+        rule.setContent {
+            val modifier = Modifier.drawBehind(onDraw) as InspectableValue
+            assertThat(modifier.nameFallback).isEqualTo("drawBehind")
+            assertThat(modifier.valueOverride).isNull()
+            assertThat(modifier.inspectableElements.map { it.name }.asIterable())
+                .containsExactly("onDraw")
+        }
+    }
+
+    @Test
+    fun testInspectorValueForDrawWithCache() {
+        val onBuildDrawCache: CacheDrawScope.() -> DrawResult = { DrawResult {} }
+        rule.setContent {
+            val modifier = Modifier.drawWithCache(onBuildDrawCache) as InspectableValue
+            assertThat(modifier.nameFallback).isEqualTo("drawWithCache")
+            assertThat(modifier.valueOverride).isNull()
+            assertThat(modifier.inspectableElements.map { it.name }.asIterable())
+                .containsExactly("onBuildDrawCache")
+        }
+    }
+
+    @Test
+    fun testInspectorValueForDrawWithContent() {
+        val onDraw: DrawScope.() -> Unit = {}
+        rule.setContent {
+            val modifier = Modifier.drawWithContent(onDraw) as InspectableValue
+            assertThat(modifier.nameFallback).isEqualTo("drawWithContent")
+            assertThat(modifier.valueOverride).isNull()
+            assertThat(modifier.inspectableElements.map { it.name }.asIterable())
+                .containsExactly("onDraw")
+        }
+    }
+
     fun SemanticsNodeInteraction.captureToBitmap() = captureToImage().asAndroidBitmap()
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
index 6d4a922c..b5ef674 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
@@ -26,6 +26,7 @@
 import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
+import androidx.compose.testutils.assertPixels
 import androidx.compose.ui.AlignTopLeft
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.AtLeastSize
@@ -44,6 +45,7 @@
 import androidx.compose.ui.graphics.Paint
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.graphics.asImageAsset
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.graphics.painter.ImagePainter
@@ -565,6 +567,35 @@
 
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
+    fun testImagePainterScalesNonUniformly() {
+        // The composable dimensions are larger than the ImageAsset. By not passing in
+        // a ContentScale parameter to the painter, the ImageAsset should be stretched
+        // non-uniformly to fully occupy the bounds of the composable
+        val boxWidth = 60
+        val boxHeight = 40
+        val srcImage = ImageAsset(10, 20)
+        val canvas = Canvas(srcImage)
+        val paint = Paint().apply { this.color = Color.Red }
+        canvas.drawRect(0f, 0f, 40f, 20f, paint)
+
+        val testTag = "testTag"
+
+        rule.setContent {
+            Box(
+                modifier = Modifier
+                    .testTag(testTag)
+                    .background(color = Color.Gray)
+                    .width((boxWidth / DensityAmbient.current.density).dp)
+                    .height((boxHeight / DensityAmbient.current.density).dp)
+                    .paint(ImagePainter(srcImage), contentScale = ContentScale.FillBounds)
+            )
+        }
+
+        rule.obtainScreenshotBitmap(boxWidth, boxHeight).asImageAsset().assertPixels { Color.Red }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
     fun testVectorPainterScalesContent() {
         // VectorPainter should handle scaling its content vector up to fill the
         // corresponding content bounds. Because the composable is twice the
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
index 742f475..4d47e25 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
@@ -27,6 +27,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.testutils.assertPixels
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.AtLeastSize
 import androidx.compose.ui.Modifier
@@ -255,6 +256,40 @@
         }
     }
 
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun testVectorScaleNonUniformly() {
+        val defaultWidth = 24.dp
+        val defaultHeight = 24.dp
+        val testTag = "testTag"
+        rule.setContent {
+            val vectorPainter = rememberVectorPainter(
+                defaultWidth = defaultWidth,
+                defaultHeight = defaultHeight
+            ) { viewportWidth, viewportHeight ->
+                Path(
+                    fill = SolidColor(Color.Blue),
+                    pathData = PathData {
+                        lineTo(viewportWidth, 0f)
+                        lineTo(viewportWidth, viewportHeight)
+                        lineTo(0f, viewportHeight)
+                        close()
+                    }
+                )
+            }
+            Image(
+                painter = vectorPainter,
+                modifier = Modifier
+                    .testTag(testTag)
+                    .preferredSize(defaultWidth * 7, defaultHeight * 3)
+                    .background(Color.Red),
+                contentScale = ContentScale.FillBounds
+            )
+        }
+
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
+    }
+
     @Composable
     private fun VectorTint(
         size: Int = 200,
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 4a42740..7f80584 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -39,6 +39,7 @@
 import androidx.compose.ui.node.OwnerScope
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
+import androidx.compose.ui.platform.ViewConfiguration
 import androidx.compose.ui.semantics.SemanticsOwner
 import androidx.compose.ui.text.font.Font
 import androidx.compose.ui.text.input.TextInputService
@@ -3209,6 +3210,9 @@
     override val measureIteration: Long
         get() = 0
 
+    override val viewConfiguration: ViewConfiguration
+        get() = TODO("Not yet implemented")
+
     override fun observeLayoutModelReads(node: LayoutNode, block: () -> Unit) {
         block()
     }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
index 6434214..3b579d6 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
@@ -18,8 +18,11 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.gesture.ExperimentalPointerInput
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.unit.Duration
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Uptime
+import androidx.compose.ui.unit.milliseconds
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -42,7 +45,7 @@
 class SuspendingPointerInputFilterTest {
     @Test
     fun testAwaitSingleEvent(): Unit = runBlocking {
-        val filter = SuspendingPointerInputFilter()
+        val filter = SuspendingPointerInputFilter(DummyViewConfiguration())
 
         val result = CompletableDeferred<PointerEvent>()
         launch {
@@ -72,7 +75,7 @@
 
     @Test
     fun testAwaitSeveralEvents(): Unit = runBlocking {
-        val filter = SuspendingPointerInputFilter()
+        val filter = SuspendingPointerInputFilter(DummyViewConfiguration())
         val results = Channel<PointerEvent>(Channel.UNLIMITED)
         val reader = launch {
             with(filter) {
@@ -110,7 +113,7 @@
 
     @Test
     fun testSyntheticCancelEvent(): Unit = runBlocking {
-        val filter = SuspendingPointerInputFilter()
+        val filter = SuspendingPointerInputFilter(DummyViewConfiguration())
         val results = Channel<PointerEvent>(Channel.UNLIMITED)
         val reader = launch {
             with(filter) {
@@ -209,4 +212,15 @@
             consumed = ConsumedData()
         ).also { previousData = current }
     }
+}
+
+private class DummyViewConfiguration : ViewConfiguration {
+    override val longPressTimeout: Duration
+        get() = 500.milliseconds
+    override val doubleTapTimeout: Duration
+        get() = 300.milliseconds
+    override val doubleTapMinTime: Duration
+        get() = 40.milliseconds
+    override val touchSlop: Float
+        get() = 18f
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LayoutIdTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LayoutIdTest.kt
index 442a04d..a5227de 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LayoutIdTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LayoutIdTest.kt
@@ -21,7 +21,6 @@
 import androidx.compose.ui.AtLeastSize
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.id
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.runOnUiThreadIR
 import androidx.compose.ui.test.TestActivity
@@ -77,9 +76,9 @@
                     }
                 ) { measurables, _ ->
                     assertEquals(3, measurables.size)
-                    assertEquals("first", measurables[0].id)
-                    assertEquals("second", measurables[1].id)
-                    assertEquals("third", measurables[2].id)
+                    assertEquals("first", measurables[0].layoutId)
+                    assertEquals("second", measurables[1].layoutId)
+                    assertEquals("third", measurables[2].layoutId)
                     latch.countDown()
                     layout(0, 0) {}
                 }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
index 8083127..25e2490 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
@@ -242,6 +242,8 @@
         }
 
     override val measureIteration: Long get() = measureAndLayoutDelegate.measureIteration
+    override val viewConfiguration: ViewConfiguration =
+        AndroidViewConfiguration(android.view.ViewConfiguration.get(context))
 
     override val hasPendingMeasureOrLayout
         get() = measureAndLayoutDelegate.hasPendingMeasureOrLayout
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
index a8ac006..3bdf895 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
@@ -234,7 +234,7 @@
         // TODO: we need a AnnotedString to CharSequence conversion function
         info.text = trimToSize(
             semanticsNode.config.getOrNull(SemanticsProperties.Text)
-                ?.toAccessibilitySpannableString(),
+                ?.toAccessibilitySpannableString(density = view.density, view.fontLoader),
             ParcelSafeTextLength
         )
         info.stateDescription =
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewConfiguration.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewConfiguration.kt
new file mode 100644
index 0000000..38b7c77
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewConfiguration.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.platform
+
+import androidx.compose.ui.unit.Duration
+import androidx.compose.ui.unit.milliseconds
+
+class AndroidViewConfiguration(
+    private val viewConfiguration: android.view.ViewConfiguration
+) : ViewConfiguration {
+    override val longPressTimeout: Duration
+        get() = android.view.ViewConfiguration.getLongPressTimeout().milliseconds
+
+    override val doubleTapTimeout: Duration
+        get() = android.view.ViewConfiguration.getDoubleTapTimeout().milliseconds
+
+    override val doubleTapMinTime: Duration
+        get() = 40.milliseconds
+
+    override val touchSlop: Float
+        get() = viewConfiguration.scaledTouchSlop.toFloat()
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt
index 087a820..3ae3b46 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt
@@ -20,6 +20,9 @@
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.Density
 
 /**
@@ -55,11 +58,20 @@
  */
 fun Modifier.drawBehind(
     onDraw: DrawScope.() -> Unit
-) = this.then(DrawBackgroundModifier(onDraw))
+) = this.then(
+    DrawBackgroundModifier(
+        >
+        inspectorInfo = debugInspectorInfo {
+            name = "drawBehind"
+            properties["onDraw"] = onDraw
+        }
+    )
+)
 
 private class DrawBackgroundModifier(
-    val onDraw: DrawScope.() -> Unit
-) : DrawModifier {
+    val onDraw: DrawScope.() -> Unit,
+    inspectorInfo: InspectorInfo.() -> Unit
+) : DrawModifier, InspectorValueInfo(inspectorInfo) {
     override fun ContentDrawScope.draw() {
         onDraw()
         drawContent()
@@ -83,7 +95,12 @@
  */
 fun Modifier.drawWithCache(
     onBuildDrawCache: CacheDrawScope.() -> DrawResult
-) = composed {
+) = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "drawWithCache"
+        properties["onBuildDrawCache"] = onBuildDrawCache
+    }
+) {
     val cacheDrawScope = remember { CacheDrawScope() }
     this.then(DrawContentCacheModifier(cacheDrawScope, onBuildDrawCache))
 }
@@ -179,8 +196,15 @@
 // TODO: Inline this function -- it breaks with current compiler
 /*inline*/ fun Modifier.drawWithContent(
     onDraw: ContentDrawScope.() -> Unit
-): Modifier = this.then(object : DrawModifier {
-    override fun ContentDrawScope.draw() {
-        onDraw()
+): Modifier = this.then(
+    object : DrawModifier, InspectorValueInfo(
+        debugInspectorInfo {
+            name = "drawWithContent"
+            properties["onDraw"] = onDraw
+        }
+    ) {
+        override fun ContentDrawScope.draw() {
+            onDraw()
+        }
     }
-})
+)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
index b609dd9..2993b7f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
@@ -27,6 +27,7 @@
 import androidx.compose.ui.graphics.drawscope.translate
 import androidx.compose.ui.graphics.painter.Painter
 import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.times
 import androidx.compose.ui.layout.IntrinsicMeasurable
 import androidx.compose.ui.layout.IntrinsicMeasureScope
 import androidx.compose.ui.layout.LayoutModifier
@@ -183,7 +184,7 @@
             }
 
             val srcSize = Size(srcWidth, srcHeight)
-            srcSize * contentScale.scale(srcSize, dstSize)
+            srcSize * contentScale.computeScaleFactor(srcSize, dstSize)
         }
     }
 
@@ -246,7 +247,7 @@
         }
 
         val srcSize = Size(srcWidth, srcHeight)
-        val scale = contentScale.scale(srcSize, size)
+        val scale = contentScale.computeScaleFactor(srcSize, size)
 
         // Compute the offset to translate the content based on the given alignment
         // and size to draw based on the ContentScale parameter
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
index c741ab1..4a42929 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
@@ -375,4 +375,18 @@
     val remainingPositionChange = this.positionChange()
     this.consumeDownChange()
     this.consumePositionChange(remainingPositionChange.x, remainingPositionChange.y)
+}
+
+/**
+ * Returns `true` if the pointer has moved outside of the region of
+ * `(0, 0, size.width, size.height)` or `false` if the current pointer is up or it is inside the
+ * given bounds.
+ */
+fun PointerInputChange.isOutOfBounds(size: IntSize): Boolean {
+    val position = current.position ?: return false
+    val x = position.x
+    val y = position.y
+    val width = size.width
+    val height = size.height
+    return x < 0f || x > width || y < 0f || y > height
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
index 21dffc8..8ac3cc1 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
@@ -24,9 +24,12 @@
 import androidx.compose.ui.composed
 import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.platform.DensityAmbient
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.platform.ViewConfigurationAmbient
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastAll
+import androidx.compose.ui.util.fastMapTo
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlin.coroutines.Continuation
 import kotlin.coroutines.ContinuationInterceptor
@@ -47,6 +50,18 @@
 @RestrictsSuspension
 interface HandlePointerInputScope {
     /**
+     * The measured size of the pointer input region. Input events will be reported with
+     * a coordinate space of (0, 0) to (size.width, size,height) as the input region, with
+     * (0, 0) indicating the upper left corner.
+     */
+    val size: IntSize
+
+    /**
+     * The state of the pointers as of the most recent event
+     */
+    val currentPointers: List<PointerInputData>
+
+    /**
      * Suspend until a [PointerEvent] is reported to the specified input [pass].
      * [pass] defaults to [PointerEventPass.Main].
      *
@@ -102,6 +117,11 @@
     val customEventDispatcher: CustomEventDispatcher
 
     /**
+     * The [ViewConfiguration] used to tune gesture detectors.
+     */
+    val viewConfiguration: ViewConfiguration
+
+    /**
      * Suspend and install a pointer input [handler] that can await input events and respond to
      * them immediately. A call to [handlePointerInput] will resume with [handler]'s result after
      * it completes.
@@ -128,7 +148,8 @@
     block: suspend PointerInputScope.() -> Unit
 ) = composed {
     val density = DensityAmbient.current
-    remember(density) { SuspendingPointerInputFilter(density) }.apply {
+    val viewConfiguration = ViewConfigurationAmbient.current
+    remember(density) { SuspendingPointerInputFilter(viewConfiguration, density) }.apply {
         LaunchedEffect(this) {
             block()
         }
@@ -155,6 +176,7 @@
 @ExperimentalPointerInput
 @OptIn(ExperimentalCollectionApi::class)
 internal class SuspendingPointerInputFilter(
+    override val viewConfiguration: ViewConfiguration,
     density: Density = Density(1f)
 ) : PointerInputFilter(),
     PointerInputModifier,
@@ -166,6 +188,8 @@
 
     private var _customEventDispatcher: CustomEventDispatcher? = null
 
+    val currentPointers = mutableListOf<PointerInputData>()
+
     /**
      * TODO: work out whether this is actually a race or not.
      * It shouldn't be, as we will have attached the [PointerInputModifier] during
@@ -252,6 +276,10 @@
         pass: PointerEventPass,
         bounds: IntSize
     ) {
+        if (pass == PointerEventPass.Initial) {
+            currentPointers.clear()
+            pointerEvent.changes.fastMapTo(currentPointers) { it.current }
+        }
         dispatchPointerEvent(pointerEvent, pass)
 
         lastPointerEvent = pointerEvent.takeIf { event ->
@@ -295,7 +323,7 @@
     override suspend fun <R> handlePointerInput(
         handler: suspend HandlePointerInputScope.() -> R
     ): R = suspendCancellableCoroutine { continuation ->
-        val handlerCoroutine = PointerEventHandlerCoroutine(continuation)
+        val handlerCoroutine = PointerEventHandlerCoroutine(continuation, currentPointers, size)
         synchronized(pointerHandlers) {
             pointerHandlers += handlerCoroutine
 
@@ -325,7 +353,9 @@
      * [ContinuationInterceptor] from the calling context and run undispatched.
      */
     private inner class PointerEventHandlerCoroutine<R>(
-        private val completion: Continuation<R>
+        private val completion: Continuation<R>,
+        override val currentPointers: List<PointerInputData>,
+        override val size: IntSize
     ) : HandlePointerInputScope, Continuation<R> {
         private var pointerAwaiter: Continuation<PointerEvent>? = null
         private var customAwaiter: Continuation<CustomEvent>? = null
@@ -374,4 +404,4 @@
             customAwaiter = continuation
         }
     }
-}
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ContentScale.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ContentScale.kt
index cbf29a5..7ecf32c3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ContentScale.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ContentScale.kt
@@ -22,8 +22,6 @@
 import kotlin.math.max
 import kotlin.math.min
 
-private const val OriginalScale = 1.0f
-
 /**
  * Represents a rule to apply to scale a source rectangle to be inscribed into a destination
  */
@@ -31,10 +29,26 @@
 interface ContentScale {
 
     /**
+     * Computes the scale factor to apply to the horizontal and vertical axes independently
+     * of one another to fit the source appropriately with the given destination
+     */
+    fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor
+
+    /**
      * Computes the scale factor to apply to both dimensions in order to fit the source
      * appropriately with the given destination size
      */
-    fun scale(srcSize: Size, dstSize: Size): Float
+    @Deprecated(
+        "use computeScaleFactor instead",
+        ReplaceWith(
+            "computeScaleFactor(srcSize, dstSize)",
+            "androidx.compose.ui.layout"
+        )
+    )
+    fun scale(srcSize: Size, dstSize: Size): Float =
+        // Returning scaleX here as previous implementations of ContentScale all provided
+        // uniform scale values which were identical for both scaleX and scaleY
+        computeScaleFactor(srcSize, dstSize).scaleX
 
     /**
      * Companion object containing commonly used [ContentScale] implementations
@@ -51,8 +65,10 @@
          */
         @Stable
         val Crop = object : ContentScale {
-            override fun scale(srcSize: Size, dstSize: Size): Float =
-                computeFillMaxDimension(srcSize, dstSize)
+            override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor =
+                computeFillMaxDimension(srcSize, dstSize).let {
+                    ScaleFactor(it, it)
+                }
         }
 
         /**
@@ -65,8 +81,10 @@
          */
         @Stable
         val Fit = object : ContentScale {
-            override fun scale(srcSize: Size, dstSize: Size): Float =
-                computeFillMinDimension(srcSize, dstSize)
+            override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor =
+                computeFillMinDimension(srcSize, dstSize).let {
+                    ScaleFactor(it, it)
+                }
         }
 
         /**
@@ -76,8 +94,10 @@
          */
         @Stable
         val FillHeight = object : ContentScale {
-            override fun scale(srcSize: Size, dstSize: Size): Float =
-                computeFillHeight(srcSize, dstSize)
+            override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor =
+                computeFillHeight(srcSize, dstSize).let {
+                    ScaleFactor(it, it)
+                }
         }
 
         /**
@@ -87,8 +107,10 @@
          */
         @Stable
         val FillWidth = object : ContentScale {
-            override fun scale(srcSize: Size, dstSize: Size): Float =
-                computeFillWidth(srcSize, dstSize)
+            override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor =
+                computeFillWidth(srcSize, dstSize).let {
+                    ScaleFactor(it, it)
+                }
         }
 
         /**
@@ -102,19 +124,37 @@
          */
         @Stable
         val Inside = object : ContentScale {
-            override fun scale(srcSize: Size, dstSize: Size): Float =
-                if (srcSize.width <= dstSize.width && srcSize.height <= dstSize.height) {
-                    OriginalScale
+
+            override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor {
+                return if (srcSize.width <= dstSize.width &&
+                    srcSize.height <= dstSize.height
+                ) {
+                    ScaleFactor(1.0f, 1.0f)
                 } else {
-                    computeFillMinDimension(srcSize, dstSize)
+                    computeFillMinDimension(srcSize, dstSize).let {
+                        ScaleFactor(it, it)
+                    }
                 }
+            }
         }
 
         /**
          * Do not apply any scaling to the source
          */
         @Stable
-        val None = FixedScale(OriginalScale)
+        val None = FixedScale(1.0f)
+
+        /**
+         * Scale horizontal and vertically non-uniformly to fill the destination bounds.
+         */
+        @Stable
+        val FillBounds = object : ContentScale {
+            override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor =
+                ScaleFactor(
+                    computeFillWidth(srcSize, dstSize),
+                    computeFillHeight(srcSize, dstSize)
+                )
+        }
     }
 }
 
@@ -124,7 +164,8 @@
  */
 @Immutable
 data class FixedScale(val value: Float) : ContentScale {
-    override fun scale(srcSize: Size, dstSize: Size): Float = value
+    override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor =
+        ScaleFactor(value, value)
 }
 
 private fun computeFillMaxDimension(srcSize: Size, dstSize: Size): Float {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutId.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutId.kt
index 0db36f9..77db358 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutId.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutId.kt
@@ -25,30 +25,30 @@
 import androidx.compose.ui.unit.Density
 
 /**
- * Tag the element with [id] to identify the element within its parent.
+ * Tag the element with [layoutId] to identify the element within its parent.
  *
  * Example usage:
  * @sample androidx.compose.ui.samples.LayoutTagChildrenUsage
  */
 @Stable
-fun Modifier.layoutId(id: Any) = this.then(
+fun Modifier.layoutId(layoutId: Any) = this.then(
     LayoutId(
-        id = id,
+        layoutId = layoutId,
         inspectorInfo = debugInspectorInfo {
             name = "layoutId"
-            value = id
+            value = layoutId
         }
     )
 )
 
 /**
- * A [ParentDataModifier] which tags the target with the given [id]. The provided tag
+ * A [ParentDataModifier] which tags the target with the given [id][layoutId]. The provided tag
  * will act as parent data, and can be used for example by parent layouts to associate
  * composable children to [Measurable]s when doing layout, as shown below.
  */
 @Immutable
 private class LayoutId(
-    override val id: Any,
+    override val layoutId: Any,
     inspectorInfo: InspectorInfo.() -> Unit
 ) : ParentDataModifier, LayoutIdParentData, InspectorValueInfo(inspectorInfo) {
     override fun Density.modifyParentData(parentData: Any?): Any? {
@@ -56,25 +56,25 @@
     }
 
     override fun hashCode(): Int =
-        id.hashCode()
+        layoutId.hashCode()
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         val otherModifier = other as? LayoutId ?: return false
-        return id == otherModifier.id
+        return layoutId == otherModifier.layoutId
     }
 
     override fun toString(): String =
-        "LayoutId(id=$id)"
+        "LayoutId(id=$layoutId)"
 }
 
 /**
  * Can be implemented by values used as parent data to make them usable as tags.
  * If a parent data value implements this interface, it can then be returned when querying
- * [Measurable.id] for the corresponding child.
+ * [Measurable.layoutId] for the corresponding child.
  */
 interface LayoutIdParentData {
-    val id: Any
+    val layoutId: Any
 }
 
 /**
@@ -85,5 +85,11 @@
  * Example usage:
  * @sample androidx.compose.ui.samples.LayoutTagChildrenUsage
  */
-val Measurable.id: Any?
-    get() = (parentData as? LayoutIdParentData)?.id
+val Measurable.layoutId: Any?
+    get() = (parentData as? LayoutIdParentData)?.layoutId
+
+@Deprecated(
+    "id was renamed to layoutId",
+    ReplaceWith("layoutId", "androidx.compose.ui.layout.layoutId")
+)
+val Measurable.id get() = layoutId
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ScaleFactor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ScaleFactor.kt
new file mode 100644
index 0000000..256ac27
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ScaleFactor.kt
@@ -0,0 +1,164 @@
+/*
+ * 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.layout
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.util.packFloats
+import androidx.compose.ui.util.toStringAsFixed
+import androidx.compose.ui.util.unpackFloat1
+import androidx.compose.ui.util.unpackFloat2
+
+/**
+ * Constructs a [ScaleFactor] from the given x and y scale values
+ */
+@Stable
+fun ScaleFactor(scaleX: Float, scaleY: Float) = ScaleFactor(packFloats(scaleX, scaleY))
+
+/**
+ * Holds 2 dimensional scaling factors for horizontal and vertical axes
+ */
+@Suppress("EXPERIMENTAL_FEATURE_WARNING")
+@Immutable
+inline class ScaleFactor(@PublishedApi internal val packedValue: Long) {
+
+    /**
+     * Returns the scale factor to apply along the horizontal axis
+     */
+    @Stable
+    val scaleX: Float
+        get() {
+            // Explicitly compare against packed values to avoid
+            // auto-boxing of ScaleFactor.Unspecified
+            check(this.packedValue != ScaleFactor.Unspecified.packedValue) {
+                "ScaleFactor is unspecified"
+            }
+            return unpackFloat1(packedValue)
+        }
+
+    /**
+     * Returns the scale factor to apply along the vertical axis
+     */
+    @Stable
+    val scaleY: Float
+        get() {
+            // Explicitly compare against packed values to avoid
+            // auto-boxing of Size.Unspecified
+            check(this.packedValue != ScaleFactor.Unspecified.packedValue) {
+                "ScaleFactor is unspecified"
+            }
+            return unpackFloat2(packedValue)
+        }
+
+    @Suppress("NOTHING_TO_INLINE")
+    @Stable
+    inline operator fun component1(): Float = scaleX
+
+    @Suppress("NOTHING_TO_INLINE")
+    @Stable
+    inline operator fun component2(): Float = scaleY
+
+    /**
+     * Returns a copy of this ScaleFactor instance optionally overriding the
+     * scaleX or scaleY parameters
+     */
+    fun copy(scaleX: Float = this.scaleX, scaleY: Float = this.scaleY) = ScaleFactor(scaleX, scaleY)
+
+    /**
+     * Multiplication operator.
+     *
+     * Returns a [ScaleFactor] with scale x and y values multiplied by the operand
+     */
+    @Stable
+    operator fun times(operand: Float) = ScaleFactor(scaleX * operand, scaleY * operand)
+
+    /**
+     * Division operator.
+     *
+     * Returns a [ScaleFactor] with scale x and y values divided by the operand
+     */
+    @Stable
+    operator fun div(operand: Float) = ScaleFactor(scaleX / operand, scaleY / operand)
+
+    override fun toString() = "ScaleFactor(${scaleX.toStringAsFixed(1)}, " +
+        "${scaleY.toStringAsFixed(1)})"
+
+    companion object {
+
+        /**
+         * A ScaleFactor whose [scaleX] and [scaleY] parameters are unspecified. This is a sentinel
+         * value used to initialize a non-null parameter.
+         * Access to scaleX or scaleY on an unspecified size is not allowed
+         */
+        @Stable
+        val Unspecified = ScaleFactor(Float.NaN, Float.NaN)
+    }
+}
+
+/**
+ * Multiplication operator with [Size].
+ *
+ * Return a new [Size] with the width and height multiplied by the [ScaleFactor.scaleX] and
+ * [ScaleFactor.scaleY] respectively
+ */
+@Stable
+operator fun Size.times(scaleFactor: ScaleFactor): Size =
+    Size(this.width * scaleFactor.scaleX, this.height * scaleFactor.scaleY)
+
+/**
+ * Multiplication operator with [Size] with reverse parameter types to maintain
+ * commutative properties of multiplication
+ *
+ * Return a new [Size] with the width and height multiplied by the [ScaleFactor.scaleX] and
+ * [ScaleFactor.scaleY] respectively
+ */
+@Stable
+operator fun ScaleFactor.times(size: Size): Size = size * this
+
+/**
+ * Division operator with [Size]
+ *
+ * Return a new [Size] with the width and height divided by [ScaleFactor.scaleX] and
+ * [ScaleFactor.scaleY] respectively
+ */
+@Stable
+operator fun Size.div(scaleFactor: ScaleFactor): Size =
+    Size(width / scaleFactor.scaleX, height / scaleFactor.scaleY)
+
+/**
+ * Linearly interpolate between two [ScaleFactor] parameters
+ *
+ * The [fraction] argument represents position on the timeline, with 0.0 meaning
+ * that the interpolation has not started, returning [start] (or something
+ * equivalent to [start]), 1.0 meaning that the interpolation has finished,
+ * returning [stop] (or something equivalent to [stop]), and values in between
+ * meaning that the interpolation is at the relevant point on the timeline
+ * between [start] and [stop]. The interpolation can be extrapolated beyond 0.0 and
+ * 1.0, so negative values and values greater than 1.0 are valid (and can
+ * easily be generated by curves).
+ *
+ * Values for [fraction] are usually obtained from an [Animation<Float>], such as
+ * an `AnimationController`.
+ */
+@Stable
+fun lerp(start: ScaleFactor, stop: ScaleFactor, fraction: Float): ScaleFactor {
+    return ScaleFactor(
+        androidx.compose.ui.util.lerp(start.scaleX, stop.scaleX, fraction),
+        androidx.compose.ui.util.lerp(start.scaleY, stop.scaleY, fraction)
+    )
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index 405f695..8fe3f76 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -26,6 +26,7 @@
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
+import androidx.compose.ui.platform.ViewConfiguration
 import androidx.compose.ui.semantics.SemanticsOwner
 import androidx.compose.ui.text.font.Font
 import androidx.compose.ui.text.input.TextInputService
@@ -195,6 +196,11 @@
 
     val measureIteration: Long
 
+    /**
+     * The [ViewConfiguration] to use in the application.
+     */
+    val viewConfiguration: ViewConfiguration
+
     companion object {
         /**
          * Enables additional (and expensive to do in production) assertions. Useful to be set
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
index d066451..a0fa226 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
@@ -102,6 +102,11 @@
  */
 val UriHandlerAmbient = staticAmbientOf<UriHandler>()
 
+/**
+ * The ambient that provides the ViewConfiguration.
+ */
+val ViewConfigurationAmbient = staticAmbientOf<ViewConfiguration>()
+
 @OptIn(ExperimentalFocus::class)
 @Composable
 internal fun ProvideCommonAmbients(
@@ -123,6 +128,7 @@
         TextInputServiceAmbient provides owner.textInputService,
         TextToolbarAmbient provides owner.textToolbar,
         UriHandlerAmbient provides uriHandler,
+        ViewConfigurationAmbient provides owner.viewConfiguration,
         children = content
     )
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ViewConfiguration.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ViewConfiguration.kt
new file mode 100644
index 0000000..7394abe
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ViewConfiguration.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.platform
+
+import androidx.compose.ui.unit.Duration
+
+/**
+ * Contains methods to standard constants used in the UI for timeouts, sizes, and distances.
+ */
+interface ViewConfiguration {
+    /**
+     * The duration before a press turns into a long press.
+     */
+    val longPressTimeout: Duration
+
+    /**
+     * The duration between the first tap's up event and the second tap's down
+     * event for an interaction to be considered a double-tap.
+     */
+    val doubleTapTimeout: Duration
+
+    /**
+     * The minimum duration between the first tap's up event and the second tap's down event for
+     * an interaction to be considered a double-tap.
+     */
+    val doubleTapMinTime: Duration
+
+    /**
+     * Distance in pixels a touch can wander before we think the user is scrolling.
+     */
+    val touchSlop: Float
+}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
index 21f84ce..801a786 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
@@ -137,6 +137,8 @@
 
     override val autofill: Autofill? get() = null
 
+    override val viewConfiguration: ViewConfiguration = DesktopViewConfiguration(density)
+
     val keyboard: Keyboard?
         get() = container.keyboard
 
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopViewConfiguration.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopViewConfiguration.kt
new file mode 100644
index 0000000..1b6c273
--- /dev/null
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopViewConfiguration.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.platform
+
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Duration
+import androidx.compose.ui.unit.milliseconds
+
+class DesktopViewConfiguration(private val density: Density) : ViewConfiguration {
+    override val longPressTimeout: Duration
+        get() = 500.milliseconds
+
+    override val doubleTapTimeout: Duration
+        get() = 300.milliseconds
+
+    override val doubleTapMinTime: Duration
+        get() = 40.milliseconds
+
+    override val touchSlop: Float
+        get() = with(density) { 18.dp.toPx() }
+}
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt
index 4ee4c20..4331d5b 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt
@@ -206,7 +206,7 @@
 }
 
 @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
-private fun compose(
+fun compose(
     recomposer: Recomposer,
     block: @Composable () -> Unit
 ): Composer<Unit> {
@@ -225,7 +225,7 @@
     }
 }
 
-private class TestFrameClock : MonotonicFrameClock {
+internal class TestFrameClock : MonotonicFrameClock {
 
     private val frameCh = Channel<Long>()
 
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/ContentScaleTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/ContentScaleTest.kt
index 8d646fc..7134996 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/ContentScaleTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/ContentScaleTest.kt
@@ -27,92 +27,112 @@
 
     @Test
     fun testScaleNone() {
-        val scale = ContentScale.None.scale(
+        val scale = ContentScale.None.computeScaleFactor(
             srcSize = Size(100.0f, 100.0f),
             dstSize = Size(200.0f, 200.0f)
         )
-        assertEquals(1.0f, scale)
+        assertEquals(1.0f, scale.scaleX)
+        assertEquals(1.0f, scale.scaleY)
     }
 
     @Test
     fun testContentScaleFit() {
-        val scale = ContentScale.Fit.scale(
+        val scale = ContentScale.Fit.computeScaleFactor(
             srcSize = Size(200.0f, 100.0f),
             dstSize = Size(100.0f, 200.0f)
         )
-        assertEquals(.5f, scale)
+        assertEquals(.5f, scale.scaleX)
+        assertEquals(.5f, scale.scaleY)
     }
 
     @Test
     fun testContentScaleFillWidth() {
-        val scale = ContentScale.FillWidth.scale(
+        val scale = ContentScale.FillWidth.computeScaleFactor(
             srcSize = Size(400.0f, 100.0f),
             dstSize = Size(100.0f, 200.0f)
         )
-        assertEquals(0.25f, scale)
+        assertEquals(0.25f, scale.scaleX)
+        assertEquals(0.25f, scale.scaleY)
     }
 
     @Test
     fun testScaleFillHeight() {
-        val scale = ContentScale.FillHeight.scale(
+        val scale = ContentScale.FillHeight.computeScaleFactor(
             srcSize = Size(400.0f, 100.0f),
             dstSize = Size(100.0f, 200.0f)
         )
-        assertEquals(2.0f, scale)
+        assertEquals(2.0f, scale.scaleX)
+        assertEquals(2.0f, scale.scaleY)
     }
 
     @Test
     fun testContentScaleCrop() {
-        val scale = ContentScale.Crop.scale(
+        val scale = ContentScale.Crop.computeScaleFactor(
             srcSize = Size(400.0f, 100.0f),
             dstSize = Size(100.0f, 200.0f)
         )
-        assertEquals(2.0f, scale)
+        assertEquals(2.0f, scale.scaleX)
+        assertEquals(2.0f, scale.scaleY)
     }
 
     @Test
     fun testContentScaleInside() {
-        val scale = ContentScale.Inside.scale(
+        val scale = ContentScale.Inside.computeScaleFactor(
             srcSize = Size(400.0f, 100.0f),
             dstSize = Size(100.0f, 200.0f)
         )
-        assertEquals(0.25f, scale)
+        assertEquals(0.25f, scale.scaleX)
+        assertEquals(0.25f, scale.scaleY)
     }
 
     @Test
     fun testContentScaleInsideLargeDst() {
         // If the src is smaller than the destination, ensure no scaling is done
-        val scale = ContentScale.Inside.scale(
+        val scale = ContentScale.Inside.computeScaleFactor(
             srcSize = Size(400.0f, 100.0f),
             dstSize = Size(900.0f, 200.0f)
         )
-        assertEquals(1.0f, scale)
+        assertEquals(1.0f, scale.scaleX)
+        assertEquals(1.0f, scale.scaleY)
     }
 
     @Test
     fun testContentFitInsideLargeDst() {
-        val scale = ContentScale.Fit.scale(
+        val scale = ContentScale.Fit.computeScaleFactor(
             srcSize = Size(400.0f, 100.0f),
             dstSize = Size(900.0f, 200.0f)
         )
-        assertEquals(2.0f, scale)
+        assertEquals(2.0f, scale.scaleX)
+        assertEquals(2.0f, scale.scaleY)
     }
 
     @Test
     fun testContentScaleCropWidth() {
-        val scale = ContentScale.Crop.scale(
+        val scale = ContentScale.Crop.computeScaleFactor(
             srcSize = Size(100.0f, 400.0f),
             dstSize = Size(200.0f, 200.0f)
         )
-        assertEquals(2.00f, scale)
+        assertEquals(2.00f, scale.scaleX)
+        assertEquals(2.00f, scale.scaleY)
     }
 
     @Test
     fun testContentScaleCropHeight() {
-        val scale = ContentScale.Crop.scale(
+        val scale = ContentScale.Crop.computeScaleFactor(
             srcSize = Size(300.0f, 100.0f),
             dstSize = Size(200.0f, 200.0f)
         )
-        assertEquals(2.00f, scale)
+        assertEquals(2.00f, scale.scaleX)
+        assertEquals(2.00f, scale.scaleY)
+    }
+
+    @Test
+    fun testContentScaleFillBoundsUp() {
+        val scale = ContentScale.FillBounds.computeScaleFactor(
+            srcSize = Size(100f, 100f),
+            dstSize = Size(300f, 700f)
+        )
+        assertEquals(3.0f, scale.scaleX)
+        assertEquals(7.0f, scale.scaleY)
     }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/ScaleFactorTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/ScaleFactorTest.kt
new file mode 100644
index 0000000..f2b8c87
--- /dev/null
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/layout/ScaleFactorTest.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.layout
+
+import androidx.compose.ui.geometry.Size
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ScaleFactorTest {
+
+    @Test
+    fun testScaleFactorConstructor() {
+        val scaleFactor = ScaleFactor(2f, 3f)
+        assertEquals(2f, scaleFactor.scaleX)
+        assertEquals(3f, scaleFactor.scaleY)
+    }
+
+    @Test
+    fun testDestructuring() {
+        val (scaleX, scaleY) = ScaleFactor(7f, 12f)
+        assertEquals(7f, scaleX)
+        assertEquals(12f, scaleY)
+    }
+
+    @Test
+    fun testCopy() {
+        val scaleFactor = ScaleFactor(11f, 4f)
+        assertEquals(scaleFactor, scaleFactor.copy())
+    }
+
+    @Test
+    fun testCopyOverwriteScaleX() {
+        val scaleFactor = ScaleFactor(7f, 2f)
+        assertEquals(ScaleFactor(3f, 2f), scaleFactor.copy(scaleX = 3f))
+    }
+
+    @Test
+    fun testCopyOverwriteScaleY() {
+        val scaleFactor = ScaleFactor(2f, 9f)
+        assertEquals(ScaleFactor(2f, 27f), scaleFactor.copy(scaleY = 27f))
+    }
+
+    @Test
+    fun testScaleFactorMultiplication() {
+        assertEquals(ScaleFactor(2f, 8f), ScaleFactor(1f, 4f) * 2f)
+    }
+
+    @Test
+    fun testScaleFactorDivision() {
+        assertEquals(ScaleFactor(1f, 4f), ScaleFactor(2f, 8f) / 2f)
+    }
+
+    @Test
+    fun testUnspecifiedScaleXQueryThrows() {
+        try {
+            ScaleFactor.Unspecified.scaleX
+            fail("Attempt to access ScaleFactor.Unspecified.scaleX is not allowed")
+        } catch (e: Throwable) {
+            // no-op
+        }
+    }
+
+    @Test
+    fun testUnspecifiedScaleYQueryThrows() {
+        try {
+            ScaleFactor.Unspecified.scaleY
+            fail("Attempt to access ScaleFactor.Unspecified.scaleY is not allowed")
+        } catch (e: Throwable) {
+            // no-op
+        }
+    }
+
+    @Test
+    fun testSizeMultiplication() {
+        val scaleFactor = ScaleFactor(2f, 3f)
+        val size = Size(100f, 200f)
+        val expected = Size(200f, 600f)
+        // verify commutative property of multiplication
+        assertEquals(expected, size * scaleFactor)
+        assertEquals(expected, scaleFactor * size)
+    }
+
+    @Test
+    fun testScaleFactorLerp() {
+        val scaleFactor1 = ScaleFactor(1f, 10f)
+        val scaleFactor2 = ScaleFactor(3f, 20f)
+        assertEquals(ScaleFactor(2f, 15f), lerp(scaleFactor1, scaleFactor2, 0.5f))
+    }
+
+    @Test
+    fun testSizeDivision() {
+        assertEquals(Size(1f, 2f), Size(100f, 300f) / ScaleFactor(100f, 150f))
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index fb9241a..ec5f1c7 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -39,6 +39,7 @@
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
+import androidx.compose.ui.platform.ViewConfiguration
 import androidx.compose.ui.semantics.SemanticsOwner
 import androidx.compose.ui.text.font.Font
 import androidx.compose.ui.text.input.TextInputService
@@ -1794,10 +1795,12 @@
     }
 
     override val measureIteration: Long = 0
+    override val viewConfiguration: ViewConfiguration
+        get() = TODO("Not yet implemented")
 }
 
 @OptIn(ExperimentalLayoutNodeApi::class)
-internal fun LayoutNode(x: Int, y: Int, x2: Int, y2: Int, modifier: Modifier = Modifier) =
+fun LayoutNode(x: Int, y: Int, x2: Int, y2: Int, modifier: Modifier = Modifier) =
     LayoutNode().apply {
         this.modifier = modifier
         measureBlocks = object : LayoutNode.NoIntrinsicsMeasureBlocks("not supported") {
diff --git a/core/core/proguard-rules.pro b/core/core/proguard-rules.pro
index 12588e6..4efb0d5 100644
--- a/core/core/proguard-rules.pro
+++ b/core/core/proguard-rules.pro
@@ -5,3 +5,6 @@
 -keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.view.WindowInsetsCompat$*Impl* {
   <methods>;
 }
+-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.app.NotificationCompat$*$Api*Impl {
+  <methods>;
+}
diff --git a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
index 7a652ed..2c40e7c 100644
--- a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
@@ -42,13 +42,16 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Color;
+import android.graphics.drawable.Icon;
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Parcelable;
 import android.support.v4.BaseInstrumentationTestCase;
 import android.widget.RemoteViews;
 
@@ -1343,6 +1346,90 @@
         assertEquals(100, n.ledOffMS);
     }
 
+    @SdkSuppress(minSdkVersion = 24)
+    @Test
+    public void testBigPictureStyle_isRecovered() {
+        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+                R.drawable.notification_bg_low_pressed);
+        Notification n = new NotificationCompat.Builder(mContext, "channelId")
+                .setSmallIcon(1)
+                .setStyle(new NotificationCompat.BigPictureStyle()
+                        .bigPicture(bitmap)
+                        .bigLargeIcon(bitmap)
+                        .setBigContentTitle("Big Content Title")
+                        .setSummaryText("Summary Text"))
+                .build();
+        Notification.Builder builder = Notification.Builder.recoverBuilder(mContext, n);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            Notification.Style style = builder.getStyle();
+            assertNotNull(style);
+            assertSame(Notification.BigPictureStyle.class, style.getClass());
+        }
+        builder.getExtras().remove(Notification.EXTRA_LARGE_ICON_BIG);
+        Icon icon = builder.build().extras.getParcelable(Notification.EXTRA_LARGE_ICON_BIG);
+        assertNotNull(icon);
+    }
+
+    @SdkSuppress(minSdkVersion = 19)
+    @Test
+    public void testBigPictureStyle_recoverStyleWithBitmap() {
+        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+                R.drawable.notification_bg_low_pressed);
+        Notification n = new Notification.Builder(mContext)
+                .setSmallIcon(1)
+                .setStyle(new Notification.BigPictureStyle()
+                        .bigPicture(bitmap)
+                        .bigLargeIcon(bitmap)
+                        .setBigContentTitle("Big Content Title")
+                        .setSummaryText("Summary Text"))
+                .build();
+        Parcelable firstBuiltIcon = n.extras.getParcelable(Notification.EXTRA_LARGE_ICON_BIG);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            assertSame(Icon.class, firstBuiltIcon.getClass());
+            assertEquals(Icon.TYPE_BITMAP, ((Icon) firstBuiltIcon).getType());
+        } else {
+            assertSame(Bitmap.class, firstBuiltIcon.getClass());
+        }
+
+        Style style = Style.extractStyleFromNotification(n);
+        assertNotNull(style);
+        assertSame(NotificationCompat.BigPictureStyle.class, style.getClass());
+        n = new NotificationCompat.Builder(mContext, "channelId")
+                .setSmallIcon(1)
+                .setStyle(style)
+                .build();
+        Parcelable rebuiltIcon = n.extras.getParcelable(Notification.EXTRA_LARGE_ICON_BIG);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            assertSame(Icon.class, rebuiltIcon.getClass());
+            assertEquals(Icon.TYPE_BITMAP, ((Icon) rebuiltIcon).getType());
+        } else {
+            assertSame(Bitmap.class, rebuiltIcon.getClass());
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 23)
+    @Test
+    public void testBigPictureStyle_recoverStyleWithResIcon() {
+        Notification n = new Notification.Builder(mContext)
+                .setSmallIcon(1)
+                .setStyle(new Notification.BigPictureStyle()
+                        .bigLargeIcon(Icon.createWithResource(mContext,
+                                R.drawable.notification_template_icon_bg)))
+                .build();
+        Icon firstBuiltIcon = n.extras.getParcelable(Notification.EXTRA_LARGE_ICON_BIG);
+        assertEquals(Icon.TYPE_RESOURCE, firstBuiltIcon.getType());
+
+        Style style = Style.extractStyleFromNotification(n);
+        assertNotNull(style);
+        assertSame(NotificationCompat.BigPictureStyle.class, style.getClass());
+        n = new NotificationCompat.Builder(mContext, "channelId")
+                .setSmallIcon(1)
+                .setStyle(style)
+                .build();
+        Icon rebuiltIcon = n.extras.getParcelable(Notification.EXTRA_LARGE_ICON_BIG);
+        assertEquals(Icon.TYPE_RESOURCE, rebuiltIcon.getType());
+    }
+
     @SdkSuppress(minSdkVersion = 16)
     @Test
     public void testMessagingStyle_nullPerson() {
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index 2df35b0..2c47026 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -2510,6 +2510,14 @@
         }
 
         /**
+         * This is called with the extras of the framework {@link Notification} during the
+         * {@link Builder#build()} process, after <code>apply()</code> has been called.  This means
+         * that you only need to add data which won't be populated by the framework Notification
+         * which was built so far.
+         *
+         * Moreover, recovering builders and styles is only supported at API 19 and above, no
+         * implementation is required for current BigTextStyle, BigPictureStyle, or InboxStyle.
+         *
          * @hide
          */
         @RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -2923,7 +2931,7 @@
                 "androidx.core.app.NotificationCompat$BigPictureStyle";
 
         private Bitmap mPicture;
-        private Bitmap mBigLargeIcon;
+        private IconCompat mBigLargeIcon;
         private boolean mBigLargeIconSet;
 
         public BigPictureStyle() {
@@ -2963,7 +2971,7 @@
          * Override the large icon when the big notification is shown.
          */
         public @NonNull BigPictureStyle bigLargeIcon(@Nullable Bitmap b) {
-            mBigLargeIcon = b;
+            mBigLargeIcon = IconCompat.createWithBitmap(b);
             mBigLargeIconSet = true;
             return this;
         }
@@ -2990,10 +2998,25 @@
                                 .setBigContentTitle(mBigContentTitle)
                                 .bigPicture(mPicture);
                 if (mBigLargeIconSet) {
-                    style.bigLargeIcon(mBigLargeIcon);
+                    if (mBigLargeIcon == null) {
+                        Api16Impl.setBigLargeIcon(style, null);
+                    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                        Context context = null;
+                        if (builder instanceof NotificationCompatBuilder) {
+                            context = ((NotificationCompatBuilder) builder).getContext();
+                        }
+                        Api23Impl.setBigLargeIcon(style, mBigLargeIcon.toIcon(context));
+                    } else if (mBigLargeIcon.getType() == IconCompat.TYPE_BITMAP) {
+                        // Before M, only the Bitmap setter existed
+                        Api16Impl.setBigLargeIcon(style, mBigLargeIcon.getBitmap());
+                    } else {
+                        // TODO(b/172282791): When we add #bigLargeIcon(Icon) we'll need to support
+                        // other icon types here by rendering them into a new Bitmap.
+                        Api16Impl.setBigLargeIcon(style, null);
+                    }
                 }
                 if (mSummaryTextSet) {
-                    style.setSummaryText(mSummaryText);
+                    Api16Impl.setSummaryText(style, mSummaryText);
                 }
             }
         }
@@ -3003,30 +3026,31 @@
          */
         @RestrictTo(LIBRARY_GROUP_PREFIX)
         @Override
-        public void addCompatExtras(@NonNull Bundle extras) {
-            super.addCompatExtras(extras);
-
-            if (mBigLargeIconSet) {
-                extras.putParcelable(EXTRA_LARGE_ICON_BIG, mBigLargeIcon);
-            }
-            extras.putParcelable(EXTRA_PICTURE, mPicture);
-        }
-
-        /**
-         * @hide
-         */
-        @RestrictTo(LIBRARY_GROUP_PREFIX)
-        @Override
         protected void restoreFromCompatExtras(@NonNull Bundle extras) {
             super.restoreFromCompatExtras(extras);
 
             if (extras.containsKey(EXTRA_LARGE_ICON_BIG)) {
-                mBigLargeIcon = extras.getParcelable(EXTRA_LARGE_ICON_BIG);
+                mBigLargeIcon = asIconCompat(extras.getParcelable(EXTRA_LARGE_ICON_BIG));
                 mBigLargeIconSet = true;
             }
             mPicture = extras.getParcelable(EXTRA_PICTURE);
         }
 
+        @Nullable
+        private static IconCompat asIconCompat(@Nullable Parcelable bitmapOrIcon) {
+            if (bitmapOrIcon != null) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                    if (bitmapOrIcon instanceof Icon) {
+                        return IconCompat.createFromIcon((Icon) bitmapOrIcon);
+                    }
+                }
+                if (bitmapOrIcon instanceof Bitmap) {
+                    return IconCompat.createWithBitmap((Bitmap) bitmapOrIcon);
+                }
+            }
+            return null;
+        }
+
         /**
          * @hide
          */
@@ -3037,6 +3061,52 @@
             extras.remove(EXTRA_LARGE_ICON_BIG);
             extras.remove(EXTRA_PICTURE);
         }
+
+        /**
+         * A class for wrapping calls to {@link Notification.BigPictureStyle} methods which
+         * were added in API 16; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(16)
+        private static class Api16Impl {
+            private Api16Impl() {
+            }
+
+            /**
+             * Calls {@link Notification.BigPictureStyle#bigLargeIcon(Bitmap)}
+             */
+            @RequiresApi(16)
+            static void setBigLargeIcon(Notification.BigPictureStyle style, Bitmap icon) {
+                style.bigLargeIcon(icon);
+            }
+
+            /**
+             * Calls {@link Notification.BigPictureStyle#setSummaryText(CharSequence)}
+             */
+            @RequiresApi(16)
+            static void setSummaryText(Notification.BigPictureStyle style, CharSequence text) {
+                style.setSummaryText(text);
+            }
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.BigPictureStyle} methods which
+         * were added in API 23; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(23)
+        private static class Api23Impl {
+            private Api23Impl() {
+            }
+
+            /**
+             * Calls {@link Notification.BigPictureStyle#bigLargeIcon(Icon)}
+             */
+            @RequiresApi(23)
+            static void setBigLargeIcon(Notification.BigPictureStyle style, Icon icon) {
+                style.bigLargeIcon(icon);
+            }
+        }
     }
 
     /**
@@ -3133,17 +3203,6 @@
          */
         @RestrictTo(LIBRARY_GROUP_PREFIX)
         @Override
-        public void addCompatExtras(@NonNull Bundle extras) {
-            super.addCompatExtras(extras);
-
-            extras.putCharSequence(EXTRA_BIG_TEXT, mBigText);
-        }
-
-        /**
-         * @hide
-         */
-        @RestrictTo(LIBRARY_GROUP_PREFIX)
-        @Override
         protected void restoreFromCompatExtras(@NonNull Bundle extras) {
             super.restoreFromCompatExtras(extras);
 
@@ -4034,18 +4093,6 @@
          */
         @RestrictTo(LIBRARY_GROUP_PREFIX)
         @Override
-        public void addCompatExtras(@NonNull Bundle extras) {
-            super.addCompatExtras(extras);
-
-            CharSequence[] arr = new CharSequence[mTexts.size()];
-            extras.putCharSequenceArray(EXTRA_TEXT_LINES, mTexts.toArray(arr));
-        }
-
-        /**
-         * @hide
-         */
-        @RestrictTo(LIBRARY_GROUP_PREFIX)
-        @Override
         protected void restoreFromCompatExtras(@NonNull Bundle extras) {
             super.restoreFromCompatExtras(extras);
             mTexts.clear();
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
index 0992927..00b1660 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
@@ -25,6 +25,7 @@
 import static androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY;
 
 import android.app.Notification;
+import android.content.Context;
 import android.os.Build;
 import android.os.Bundle;
 import android.text.TextUtils;
@@ -46,6 +47,7 @@
  */
 @RestrictTo(LIBRARY_GROUP_PREFIX)
 class NotificationCompatBuilder implements NotificationBuilderWithBuilderAccessor {
+    private final Context mContext;
     private final Notification.Builder mBuilder;
     private final NotificationCompat.Builder mBuilderCompat;
 
@@ -65,6 +67,7 @@
     @SuppressWarnings("deprecation")
     NotificationCompatBuilder(NotificationCompat.Builder b) {
         mBuilderCompat = b;
+        mContext = b.mContext;
         if (Build.VERSION.SDK_INT >= 26) {
             mBuilder = new Notification.Builder(b.mContext, b.mChannelId);
         } else {
@@ -299,6 +302,10 @@
         return mBuilder;
     }
 
+    Context getContext() {
+        return mContext;
+    }
+
     public Notification build() {
         final NotificationCompat.Style style = mBuilderCompat.mStyle;
         if (style != null) {
diff --git a/development/build_log_processor.sh b/development/build_log_processor.sh
index 57bbc84..46257dd 100755
--- a/development/build_log_processor.sh
+++ b/development/build_log_processor.sh
@@ -47,13 +47,26 @@
 SCRIPT_PATH="$(cd $(dirname $0) && pwd)"
 CHECKOUT="$(cd "$SCRIPT_PATH/../../.." && pwd)"
 if [ -n "$DIST_DIR" ]; then
-  LOG_DIR="$DIST_DIR"
+  LOG_DIR="$DIST_DIR/logs"
 else
-  LOG_DIR="$CHECKOUT/out/dist"
+  LOG_DIR="$CHECKOUT/out/dist/logs"
 fi
 
 mkdir -p "$LOG_DIR"
 logFile="$LOG_DIR/gradle.log"
+
+# Move any preexisting $logFile to make room for a new one
+# After moving $logFile several times it eventually gets deleted
+function rotateLogs() {
+  iPlus1="10"
+  for i in $(seq 9 -1 1); do
+    mv "$LOG_DIR/gradle.${i}.log" "$LOG_DIR/gradle.${iPlus1}.log" 2>/dev/null || true
+    iPlus1=$i
+  done
+  mv $logFile "$LOG_DIR/gradle.1.log" 2>/dev/null || true
+}
+rotateLogs
+
 rm -f "$logFile"
 # Save OUT_DIR and some other variables into the log file so build_log_simplifier.py can
 # identify them later
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 822a349..2fce42b 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -16,70 +16,72 @@
     docs("androidx.arch.core:core-runtime:2.1.0")
     docs("androidx.arch.core:core-testing:2.1.0")
     docs("androidx.asynclayoutinflater:asynclayoutinflater:1.0.0")
-    docs("androidx.autofill:autofill:1.1.0-beta01")
+    docs("androidx.autofill:autofill:1.1.0-rc01")
     docs("androidx.benchmark:benchmark-common:1.1.0-alpha01")
     docs("androidx.benchmark:benchmark-junit4:1.1.0-alpha01")
-    docs("androidx.biometric:biometric:1.1.0-beta01")
+    docs("androidx.biometric:biometric:1.1.0-rc01")
     docs("androidx.browser:browser:1.3.0-beta01")
-    docs("androidx.camera:camera-camera2:1.0.0-beta11")
-    docs("androidx.camera:camera-core:1.0.0-beta11")
-    docs("androidx.camera:camera-extensions:1.0.0-alpha18")
+    docs("androidx.camera:camera-camera2:1.0.0-beta12")
+    docs("androidx.camera:camera-core:1.0.0-beta12")
+    docs("androidx.camera:camera-extensions:1.0.0-alpha19")
     stubs(fileTree(dir: '../camera/camera-extensions-stub', include: ['camera-extensions-stub.jar']))
-    docs("androidx.camera:camera-lifecycle:1.0.0-beta11")
-    docs("androidx.camera:camera-view:1.0.0-alpha18")
+    docs("androidx.camera:camera-lifecycle:1.0.0-beta12")
+    docs("androidx.camera:camera-view:1.0.0-alpha19")
     docs("androidx.cardview:cardview:1.0.0")
     docs("androidx.collection:collection:1.1.0")
     docs("androidx.collection:collection-ktx:1.1.0")
-    docs("androidx.compose.animation:animation:1.0.0-alpha06")
-    docs("androidx.compose.animation:animation-core:1.0.0-alpha06")
-    samples("androidx.compose.animation:animation-samples:1.0.0-alpha06")
-    samples("androidx.compose.animation:animation-core-samples:1.0.0-alpha06")
-    docs("androidx.compose.foundation:foundation:1.0.0-alpha06")
-    docs("androidx.compose.foundation:foundation-layout:1.0.0-alpha06")
-    samples("androidx.compose.foundation:foundation-layout-samples:1.0.0-alpha06")
-    docs("androidx.compose.foundation:foundation-text:1.0.0-alpha06")
-    samples("androidx.compose.foundation:foundation-text-samples:1.0.0-alpha06")
-    samples("androidx.compose.foundation:foundation-samples:1.0.0-alpha06")
-    docs("androidx.compose.material:material:1.0.0-alpha06")
-    docs("androidx.compose.material:material-icons-core:1.0.0-alpha06")
-    samples("androidx.compose.material:material-icons-core-samples:1.0.0-alpha06")
-    docs("androidx.compose.material:material-icons-extended:1.0.0-alpha06")
-    samples("androidx.compose.material:material-samples:1.0.0-alpha06")
-    docs("androidx.compose.runtime:runtime:1.0.0-alpha06")
-    docs("androidx.compose.runtime:runtime-dispatch:1.0.0-alpha06")
-    docs("androidx.compose.runtime:runtime-livedata:1.0.0-alpha06")
-    samples("androidx.compose.runtime:runtime-livedata-samples:1.0.0-alpha06")
-    docs("androidx.compose.runtime:runtime-rxjava2:1.0.0-alpha06")
-    samples("androidx.compose.runtime:runtime-rxjava2-samples:1.0.0-alpha06")
-    docs("androidx.compose.runtime:runtime-saved-instance-state:1.0.0-alpha06")
-    samples("androidx.compose.runtime:runtime-saved-instance-state-samples:1.0.0-alpha06")
-    samples("androidx.compose.runtime:runtime-samples:1.0.0-alpha06")
-    docs("androidx.compose.ui:ui:1.0.0-alpha06")
-    docs("androidx.compose.ui:ui-geometry:1.0.0-alpha06")
-    docs("androidx.compose.ui:ui-graphics:1.0.0-alpha06")
-    samples("androidx.compose.ui:ui-graphics-samples:1.0.0-alpha06")
-    docs("androidx.compose.ui:ui-text:1.0.0-alpha06")
+    docs("androidx.compose.animation:animation:1.0.0-alpha07")
+    docs("androidx.compose.animation:animation-core:1.0.0-alpha07")
+    samples("androidx.compose.animation:animation-samples:1.0.0-alpha07")
+    samples("androidx.compose.animation:animation-core-samples:1.0.0-alpha07")
+    docs("androidx.compose.foundation:foundation:1.0.0-alpha07")
+    docs("androidx.compose.foundation:foundation-layout:1.0.0-alpha07")
+    samples("androidx.compose.foundation:foundation-layout-samples:1.0.0-alpha07")
+    docs("androidx.compose.foundation:foundation-text:1.0.0-alpha07")
+    samples("androidx.compose.foundation:foundation-text-samples:1.0.0-alpha07")
+    samples("androidx.compose.foundation:foundation-samples:1.0.0-alpha07")
+    docs("androidx.compose.material:material:1.0.0-alpha07")
+    docs("androidx.compose.material:material-icons-core:1.0.0-alpha07")
+    samples("androidx.compose.material:material-icons-core-samples:1.0.0-alpha07")
+    docs("androidx.compose.material:material-icons-extended:1.0.0-alpha07")
+    samples("androidx.compose.material:material-samples:1.0.0-alpha07")
+    docs("androidx.compose.runtime:runtime:1.0.0-alpha07")
+    docs("androidx.compose.runtime:runtime-dispatch:1.0.0-alpha07")
+    docs("androidx.compose.runtime:runtime-livedata:1.0.0-alpha07")
+    samples("androidx.compose.runtime:runtime-livedata-samples:1.0.0-alpha07")
+    docs("androidx.compose.runtime:runtime-rxjava2:1.0.0-alpha07")
+    samples("androidx.compose.runtime:runtime-rxjava2-samples:1.0.0-alpha07")
+    docs("androidx.compose.runtime:runtime-saved-instance-state:1.0.0-alpha07")
+    samples("androidx.compose.runtime:runtime-saved-instance-state-samples:1.0.0-alpha07")
+    samples("androidx.compose.runtime:runtime-samples:1.0.0-alpha07")
+    docs("androidx.compose.ui:ui:1.0.0-alpha07")
+    docs("androidx.compose.ui:ui-geometry:1.0.0-alpha07")
+    docs("androidx.compose.ui:ui-graphics:1.0.0-alpha07")
+    samples("androidx.compose.ui:ui-graphics-samples:1.0.0-alpha07")
+    docs("androidx.compose.ui:ui-text:1.0.0-alpha07")
     docs("androidx.compose.ui:ui-text-android:1.0.0-alpha06")
-    samples("androidx.compose.ui:ui-text-samples:1.0.0-alpha06")
-    docs("androidx.compose.ui:ui-unit:1.0.0-alpha06")
-    samples("androidx.compose.ui:ui-unit-samples:1.0.0-alpha06")
-    docs("androidx.compose.ui:ui-util:1.0.0-alpha06")
-    docs("androidx.compose.ui:ui-viewbinding:1.0.0-alpha06")
-    samples("androidx.compose.ui:ui-viewbinding-samples:1.0.0-alpha06")
-    samples("androidx.compose.ui:ui-samples:1.0.0-alpha06")
+    samples("androidx.compose.ui:ui-text-samples:1.0.0-alpha07")
+    docs("androidx.compose.ui:ui-unit:1.0.0-alpha07")
+    samples("androidx.compose.ui:ui-unit-samples:1.0.0-alpha07")
+    docs("androidx.compose.ui:ui-util:1.0.0-alpha07")
+    docs("androidx.compose.ui:ui-viewbinding:1.0.0-alpha07")
+    samples("androidx.compose.ui:ui-viewbinding-samples:1.0.0-alpha07")
+    samples("androidx.compose.ui:ui-samples:1.0.0-alpha07")
     docs("androidx.concurrent:concurrent-futures:1.1.0")
     docs("androidx.concurrent:concurrent-futures-ktx:1.1.0")
     docs("androidx.contentpager:contentpager:1.0.0")
     docs("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
     docs("androidx.core:core-role:1.1.0-alpha02")
-    docs("androidx.core:core:1.5.0-alpha04")
+    docs("androidx.core:core:1.5.0-alpha05")
     docs("androidx.core:core-animation:1.0.0-alpha02")
     docs("androidx.core:core-animation-testing:1.0.0-alpha02")
-    docs("androidx.core:core-ktx:1.5.0-alpha04")
+    docs("androidx.core:core-ktx:1.5.0-alpha05")
     docs("androidx.cursoradapter:cursoradapter:1.0.0")
     docs("androidx.customview:customview:1.1.0")
-    docs("androidx.datastore:datastore-core:1.0.0-alpha02")
-    docs("androidx.datastore:datastore-preferences:1.0.0-alpha02")
+    docs("androidx.datastore:datastore:1.0.0-alpha03")
+    docs("androidx.datastore:datastore-core:1.0.0-alpha03")
+    docs("androidx.datastore:datastore-preferences:1.0.0-alpha03")
+    docs("androidx.datastore:datastore-preferences-core:1.0.0-alpha03")
     docs("androidx.documentfile:documentfile:1.0.0")
     docs("androidx.drawerlayout:drawerlayout:1.1.1")
     docs("androidx.dynamicanimation:dynamicanimation:1.1.0-alpha02")
@@ -129,7 +131,7 @@
     docs("androidx.mediarouter:mediarouter:1.2.0")
     docs("androidx.navigation:navigation-common:2.3.1")
     docs("androidx.navigation:navigation-common-ktx:2.3.1")
-    docs("androidx.navigation:navigation-compose:1.0.0-alpha01")
+    docs("androidx.navigation:navigation-compose:1.0.0-alpha02")
     samples("androidx.navigation:navigation-compose-samples:1.0.0-alpha01")
     docs("androidx.navigation:navigation-dynamic-features-fragment:2.3.1")
     docs("androidx.navigation:navigation-dynamic-features-runtime:2.3.1")
@@ -140,17 +142,17 @@
     docs("androidx.navigation:navigation-testing:2.3.1")
     docs("androidx.navigation:navigation-ui:2.3.1")
     docs("androidx.navigation:navigation-ui-ktx:2.3.1")
-    docs("androidx.paging:paging-common:3.0.0-alpha08")
-    docs("androidx.paging:paging-common-ktx:3.0.0-alpha08")
-    docs("androidx.paging:paging-compose:1.0.0-alpha01")
+    docs("androidx.paging:paging-common:3.0.0-alpha09")
+    docs("androidx.paging:paging-common-ktx:3.0.0-alpha09")
+    docs("androidx.paging:paging-compose:1.0.0-alpha02")
     samples("androidx.paging:paging-compose-samples:3.0.0-alpha08")
-    docs("androidx.paging:paging-guava:3.0.0-alpha08")
-    docs("androidx.paging:paging-runtime:3.0.0-alpha08")
-    docs("androidx.paging:paging-runtime-ktx:3.0.0-alpha08")
-    docs("androidx.paging:paging-rxjava2:3.0.0-alpha08")
-    docs("androidx.paging:paging-rxjava2-ktx:3.0.0-alpha08")
-    docs("androidx.paging:paging-rxjava3:3.0.0-alpha08")
-    samples("androidx.paging:paging-samples:3.0.0-alpha08")
+    docs("androidx.paging:paging-guava:3.0.0-alpha09")
+    docs("androidx.paging:paging-runtime:3.0.0-alpha09")
+    docs("androidx.paging:paging-runtime-ktx:3.0.0-alpha09")
+    docs("androidx.paging:paging-rxjava2:3.0.0-alpha09")
+    docs("androidx.paging:paging-rxjava2-ktx:3.0.0-alpha09")
+    docs("androidx.paging:paging-rxjava3:3.0.0-alpha09")
+    samples("androidx.paging:paging-samples:3.0.0-alpha09")
     docs("androidx.palette:palette:1.0.0")
     docs("androidx.palette:palette-ktx:1.0.0")
     docs("androidx.percentlayout:percentlayout:1.0.1")
@@ -196,19 +198,21 @@
     docs("androidx.versionedparcelable:versionedparcelable:1.1.1")
     docs("androidx.viewpager2:viewpager2:1.1.0-alpha01")
     docs("androidx.viewpager:viewpager:1.0.0")
-    docs("androidx.wear:wear:1.1.0")
+    docs("androidx.wear:wear:1.2.0-alpha02")
     stubs(fileTree(dir: '../wear/wear_stubs/', include: ['com.google.android.wearable-stubs.jar']))
-    docs("androidx.wear:wear-complications-data:1.0.0-alpha01")
-    docs("androidx.wear:wear-complications-provider:1.0.0-alpha01")
-    docs("androidx.wear:wear-complications-rendering:1.0.0-alpha01")
-    docs("androidx.wear:wear-watchface:1.0.0-alpha01")
-    docs("androidx.wear:wear-watchface-complications-rendering:1.0.0-alpha01")
-    docs("androidx.wear:wear-watchface-data:1.0.0-alpha01")
-    samples("androidx.wear:wear-watchface-samples:1.0.0-alpha01")
-    docs("androidx.wear:wear-watchface-style:1.0.0-alpha01")
-    docs("androidx.wear:wear-input:1.0.0-alpha01")
-    docs("androidx.wear:wear-input-testing:1.0.0-alpha01")
-    docs("androidx.webkit:webkit:1.4.0-beta01")
+    docs("androidx.wear:wear-complications-data:1.0.0-alpha02")
+    docs("androidx.wear:wear-complications-provider:1.0.0-alpha02")
+    docs("androidx.wear:wear-tiles:1.0.0-alpha01")
+    docs("androidx.wear:wear-tiles-data:1.0.0-alpha01")
+    docs("androidx.wear:wear-watchface:1.0.0-alpha02")
+    docs("androidx.wear:wear-watchface-client:1.0.0-alpha02")
+    docs("androidx.wear:wear-watchface-complications-rendering:1.0.0-alpha02")
+    docs("androidx.wear:wear-watchface-data:1.0.0-alpha02")
+    samples("androidx.wear:wear-watchface-samples:1.0.0-alpha02")
+    docs("androidx.wear:wear-watchface-style:1.0.0-alpha02")
+    docs("androidx.wear:wear-input:1.0.0-rc01")
+    docs("androidx.wear:wear-input-testing:1.0.0-rc01")
+    docs("androidx.webkit:webkit:1.4.0-rc01")
     docs("androidx.window:window:1.0.0-alpha01")
     stubs(fileTree(dir: '../window/stubs/', include: ['window-sidecar-release-0.1.0-alpha01.aar']))
     stubs(project(":window:window-extensions"))
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
index 87b46f8..28269ba 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
@@ -19,7 +19,6 @@
 import android.os.Build
 import android.os.Bundle
 import android.transition.Transition
-import android.transition.TransitionListenerAdapter
 import android.transition.TransitionSet
 import android.view.View
 import androidx.core.app.SharedElementCallback
@@ -738,17 +737,27 @@
         fragment2.allowEnterTransitionOverlap = false
         fragment2.enterTransition.setRealTransition(true)
         var enterTransitionStarted = false
-        fragment2.enterTransition.addListener(object : TransitionListenerAdapter() {
+        fragment2.enterTransition.addListener(object : Transition.TransitionListener {
             override fun onTransitionStart(transition: Transition?) {
                 enterTransitionStarted = true
             }
+
+            override fun onTransitionEnd(transition: Transition?) { }
+            override fun onTransitionCancel(transition: Transition?) { }
+            override fun onTransitionPause(transition: Transition?) { }
+            override fun onTransitionResume(transition: Transition?) { }
         })
         var enterTransitionStartedOnEnd = true
         fragment.exitTransition.setRealTransition(true)
-        fragment.exitTransition.addListener(object : TransitionListenerAdapter() {
+        fragment.exitTransition.addListener(object : Transition.TransitionListener {
             override fun onTransitionEnd(transition: Transition?) {
                 enterTransitionStartedOnEnd = enterTransitionStarted
             }
+
+            override fun onTransitionStart(transition: Transition?) { }
+            override fun onTransitionCancel(transition: Transition?) { }
+            override fun onTransitionPause(transition: Transition?) { }
+            override fun onTransitionResume(transition: Transition?) { }
         })
 
         fragmentManager.beginTransaction()
diff --git a/gradlew b/gradlew
index f567cdb..00d0aeb 100755
--- a/gradlew
+++ b/gradlew
@@ -39,6 +39,8 @@
     # and doesn't set DIST_DIR and we want gradlew and Studio to match
 fi
 
+# unset ANDROID_BUILD_TOP so that Lint doesn't think we're building the platform itself
+unset ANDROID_BUILD_TOP
 # ----------------------------------------------------------------------------
 
 # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
@@ -266,5 +268,9 @@
 # Check whether we were given the "-PverifyUpToDate" argument
 if [[ " ${@} " =~ " -PverifyUpToDate " ]]; then
   # Re-run Gradle, and find all tasks that are unexpectly out of date
-  runGradle "$@" -PdisallowExecution --continue
+  if ! runGradle "$@" -PdisallowExecution --continue; then
+    echo >&2
+    echo "TaskUpToDateValidator's second build failed, -PdisallowExecution specified" >&2
+    exit 1
+  fi
 fi
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaLibrarySessionImplBase.java b/media2/session/src/main/java/androidx/media2/session/MediaLibrarySessionImplBase.java
index 8beeff5..a624d1d 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaLibrarySessionImplBase.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaLibrarySessionImplBase.java
@@ -58,7 +58,7 @@
     }
 
     @Override
-    MediaBrowserServiceCompat createLegacyBrowserService(Context context, SessionToken token,
+    MediaBrowserServiceCompat createLegacyBrowserServiceLocked(Context context, SessionToken token,
             Token sessionToken) {
         return new MediaLibraryServiceLegacyStub(context, this, sessionToken);
     }
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java b/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
index 7532e05..d1db6dd 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
@@ -266,13 +266,13 @@
             mPlayer = player;
             mPlaybackInfo = info;
             mVolumeProviderCompat = volumeProviderCompat;
+        }
 
-            if (oldPlayer != mPlayer) {
-                if (oldPlayer != null) {
-                    oldPlayer.unregisterPlayerCallback(mPlayerCallback);
-                }
-                mPlayer.registerPlayerCallback(mCallbackExecutor, mPlayerCallback);
+        if (oldPlayer != player) {
+            if (oldPlayer != null) {
+                oldPlayer.unregisterPlayerCallback(mPlayerCallback);
             }
+            player.registerPlayerCallback(mCallbackExecutor, mPlayerCallback);
         }
 
         if (oldPlayer == null) {
@@ -354,6 +354,7 @@
 
     @Override
     public void close() {
+        SessionPlayer player;
         synchronized (mLock) {
             if (mClosed) {
                 return;
@@ -363,26 +364,28 @@
                 Log.d(TAG, "Closing session, id=" + getId() + ", token="
                         + getToken());
             }
-            mPlayer.unregisterPlayerCallback(mPlayerCallback);
-            mSessionCompat.release();
-            mMediaButtonIntent.cancel();
-            if (mBroadcastReceiver != null) {
-                mContext.unregisterReceiver(mBroadcastReceiver);
+
+            player = mPlayer;
+        }
+        player.unregisterPlayerCallback(mPlayerCallback);
+        mSessionCompat.release();
+        mMediaButtonIntent.cancel();
+        if (mBroadcastReceiver != null) {
+            mContext.unregisterReceiver(mBroadcastReceiver);
+        }
+        mCallback.onSessionClosed(mInstance);
+        dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
+            @Override
+            public void run(ControllerCb callback, int seq) throws RemoteException {
+                callback.onDisconnected(seq);
             }
-            mCallback.onSessionClosed(mInstance);
-            dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
-                @Override
-                public void run(ControllerCb callback, int seq) throws RemoteException {
-                    callback.onDisconnected(seq);
-                }
-            });
-            mHandler.removeCallbacksAndMessages(null);
-            if (mHandlerThread.isAlive()) {
-                if (Build.VERSION.SDK_INT >= 18) {
-                    mHandlerThread.quitSafely();
-                } else {
-                    mHandlerThread.quit();
-                }
+        });
+        mHandler.removeCallbacksAndMessages(null);
+        if (mHandlerThread.isAlive()) {
+            if (Build.VERSION.SDK_INT >= 18) {
+                mHandlerThread.quitSafely();
+            } else {
+                mHandlerThread.quit();
             }
         }
     }
@@ -986,35 +989,33 @@
 
     @Override
     public PlaybackStateCompat createPlaybackStateCompat() {
-        synchronized (mLock) {
-            int state = MediaUtils.convertToPlaybackStateCompatState(getPlayerState(),
-                    getBufferingState());
-            long allActions = PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PAUSE
-                    | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_REWIND
-                    | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
-                    | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
-                    | PlaybackStateCompat.ACTION_FAST_FORWARD
-                    | PlaybackStateCompat.ACTION_SET_RATING
-                    | PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_PLAY_PAUSE
-                    | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
-                    | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
-                    | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
-                    | PlaybackStateCompat.ACTION_PLAY_FROM_URI | PlaybackStateCompat.ACTION_PREPARE
-                    | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
-                    | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
-                    | PlaybackStateCompat.ACTION_PREPARE_FROM_URI
-                    | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
-                    | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
-                    | PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED;
-            long queueItemId = MediaUtils.convertToQueueItemId(getCurrentMediaItemIndex());
-            return new PlaybackStateCompat.Builder()
-                    .setState(state, getCurrentPosition(), getPlaybackSpeed(),
-                            SystemClock.elapsedRealtime())
-                    .setActions(allActions)
-                    .setActiveQueueItemId(queueItemId)
-                    .setBufferedPosition(getBufferedPosition())
-                    .build();
-        }
+        int state = MediaUtils.convertToPlaybackStateCompatState(getPlayerState(),
+                getBufferingState());
+        long allActions = PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PAUSE
+                | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_REWIND
+                | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+                | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+                | PlaybackStateCompat.ACTION_FAST_FORWARD
+                | PlaybackStateCompat.ACTION_SET_RATING
+                | PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_PLAY_PAUSE
+                | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
+                | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
+                | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
+                | PlaybackStateCompat.ACTION_PLAY_FROM_URI | PlaybackStateCompat.ACTION_PREPARE
+                | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
+                | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
+                | PlaybackStateCompat.ACTION_PREPARE_FROM_URI
+                | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
+                | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
+                | PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED;
+        long queueItemId = MediaUtils.convertToQueueItemId(getCurrentMediaItemIndex());
+        return new PlaybackStateCompat.Builder()
+                .setState(state, getCurrentPosition(), getPlaybackSpeed(),
+                        SystemClock.elapsedRealtime())
+                .setActions(allActions)
+                .setActiveQueueItemId(queueItemId)
+                .setBufferedPosition(getBufferedPosition())
+                .build();
     }
 
     @Override
@@ -1029,7 +1030,7 @@
         return mSessionActivity;
     }
 
-    MediaBrowserServiceCompat createLegacyBrowserService(Context context, SessionToken token,
+    MediaBrowserServiceCompat createLegacyBrowserServiceLocked(Context context, SessionToken token,
             Token sessionToken) {
         return new MediaSessionServiceLegacyStub(context, this, sessionToken);
     }
@@ -1051,8 +1052,8 @@
         MediaBrowserServiceCompat legacyStub;
         synchronized (mLock) {
             if (mBrowserServiceLegacyStub == null) {
-                mBrowserServiceLegacyStub = createLegacyBrowserService(mContext, mSessionToken,
-                        mSessionCompat.getSessionToken());
+                mBrowserServiceLegacyStub = createLegacyBrowserServiceLocked(mContext,
+                        mSessionToken, mSessionCompat.getSessionToken());
             }
             legacyStub = mBrowserServiceLegacyStub;
         }
@@ -1366,7 +1367,7 @@
             MediaItem.OnMetadataChangedListener {
         private final WeakReference<MediaSessionImplBase> mSession;
         private MediaItem mMediaItem;
-        private List<MediaItem> mList;
+        private List<MediaItem> mPlaylist;
         private final PlaylistItemListener mPlaylistItemChangedListener;
 
         SessionPlayerCallback(MediaSessionImplBase session) {
@@ -1381,15 +1382,13 @@
             if (session == null || player == null || session.getPlayer() != player) {
                 return;
             }
-            synchronized (session.mLock) {
-                if (mMediaItem != null) {
-                    mMediaItem.removeOnMetadataChangedListener(this);
-                }
-                if (item != null) {
-                    item.addOnMetadataChangedListener(session.mCallbackExecutor, this);
-                }
-                mMediaItem = item;
+            if (mMediaItem != null) {
+                mMediaItem.removeOnMetadataChangedListener(this);
             }
+            if (item != null) {
+                item.addOnMetadataChangedListener(session.mCallbackExecutor, this);
+            }
+            mMediaItem = item;
 
             boolean notifyingPended = false;
             if (item != null) {
@@ -1461,20 +1460,18 @@
             if (session == null || player == null || session.getPlayer() != player) {
                 return;
             }
-            synchronized (session.mLock) {
-                if (mList != null) {
-                    for (int i = 0; i < mList.size(); i++) {
-                        mList.get(i).removeOnMetadataChangedListener(mPlaylistItemChangedListener);
-                    }
+            if (mPlaylist != null) {
+                for (int i = 0; i < mPlaylist.size(); i++) {
+                    mPlaylist.get(i).removeOnMetadataChangedListener(mPlaylistItemChangedListener);
                 }
-                if (list != null) {
-                    for (int i = 0; i < list.size(); i++) {
-                        list.get(i).addOnMetadataChangedListener(session.mCallbackExecutor,
-                                mPlaylistItemChangedListener);
-                    }
-                }
-                mList = list;
             }
+            if (list != null) {
+                for (int i = 0; i < list.size(); i++) {
+                    list.get(i).addOnMetadataChangedListener(session.mCallbackExecutor,
+                            mPlaylistItemChangedListener);
+                }
+            }
+            mPlaylist = list;
 
             dispatchRemoteControllerTask(player, new RemoteControllerTask() {
                 @Override
diff --git a/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java b/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
index e932706..6b8a632 100644
--- a/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
+++ b/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
@@ -94,7 +94,8 @@
 
         Bundle testHints = new Bundle();
         testHints.putString("test_key", "test_value");
-        RemoteMediaController controller = createRemoteController(mToken, true, testHints);
+        RemoteMediaController controller = createRemoteController(
+                mToken, /*waitForConnection=*/false, testHints);
 
         // onGetSession() should be called.
         assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
diff --git a/media2/session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java b/media2/session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
index 0578ef8..a251a80 100644
--- a/media2/session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
+++ b/media2/session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
@@ -93,7 +93,8 @@
 
         Bundle testHints = new Bundle();
         testHints.putString("test_key", "test_value");
-        RemoteMediaController controller = createRemoteController(mToken, true, testHints);
+        RemoteMediaController controller = createRemoteController(
+                mToken, /*waitForConnection=*/false, testHints);
 
         // onGetSession() should be called.
         assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 0f3208c..e8377ac8 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -38,6 +38,7 @@
     api project(":compose:ui:ui")
     api "androidx.navigation:navigation-runtime-ktx:2.3.1"
 
+    androidTestImplementation project(":compose:material:material")
     androidTestImplementation 'androidx.navigation:navigation-testing:2.3.1'
     androidTestImplementation project(path: ':internal-testutils-navigation'), {
         exclude group: 'androidx.navigation', module: 'navigation-common-ktx'
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt
new file mode 100644
index 0000000..fd00b02
--- /dev/null
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.navigation.compose.demos
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonConstants
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.navigate
+import androidx.navigation.compose.popUpTo
+import androidx.navigation.compose.rememberNavController
+
+@Composable
+fun NavPopUpToDemo() {
+    val navController = rememberNavController()
+    NavHost(navController, startDestination = "1") {
+        composable("1") { NumberedScreen(navController, 1) }
+        composable("2") { NumberedScreen(navController, 2) }
+        composable("3") { NumberedScreen(navController, 3) }
+        composable("4") { NumberedScreen(navController, 4) }
+        composable("5") { NumberedScreen(navController, 5) }
+    }
+}
+
+@Composable
+fun NumberedScreen(navController: NavController, number: Int) {
+    Column(Modifier.fillMaxSize().then(Modifier.padding(8.dp))) {
+        val next = number + 1
+        if (number < 5) {
+            Button(
+                 navController.navigate("$next") },
+                colors = ButtonConstants.defaultButtonColors(backgroundColor = Color.LightGray),
+                modifier = Modifier.fillMaxWidth()
+            ) {
+                Text(text = "Navigate to Screen $next")
+            }
+        }
+        Text("This is screen $number", Modifier.weight(1f))
+        if (navController.previousBackStackEntry != null) {
+            Button(
+                 navController.navigate("1") { popUpTo("1") { inclusive = true } } },
+                colors = ButtonConstants.defaultButtonColors(backgroundColor = Color.LightGray),
+                modifier = Modifier.fillMaxWidth()
+            ) {
+                Text(text = "PopUpTo Screen 1")
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavSingleTopDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavSingleTopDemo.kt
new file mode 100644
index 0000000..05f2abc
--- /dev/null
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavSingleTopDemo.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.navigation.compose.demos
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Divider
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.savedinstancestate.savedInstanceState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.navigate
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.compose.samples.NavigateButton
+
+@Composable
+fun NavSingleTopDemo() {
+    val navController = rememberNavController()
+    val query = savedInstanceState(saver = TextFieldValue.Saver) { TextFieldValue() }
+    Column(Modifier.fillMaxSize().then(Modifier.padding(8.dp))) {
+        TextField(
+            value = query.value,
+             query.value = it },
+            placeholder = { Text("Search") }
+        )
+        NavigateButton("Search") {
+            navController.navigate("search/" + query.value.text) {
+                launchSingleTop = true
+            }
+        }
+        NavHost(navController, startDestination = "start") {
+            composable("start") { StartScreen() }
+            composable("search/{query}") { backStackEntry ->
+                SearchResultScreen(
+                    backStackEntry.arguments!!.getString("query", "no query entered")
+                )
+            }
+        }
+    }
+}
+
+@Composable
+fun StartScreen() {
+    Divider(color = Color.Black)
+    Text(text = "Start a search above")
+}
+
+@Composable
+fun SearchResultScreen(query: String) {
+    Text("You searched for $query")
+}
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavigationDemos.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavigationDemos.kt
index e1d2725..cca6694 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavigationDemos.kt
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavigationDemos.kt
@@ -26,6 +26,8 @@
         ComposableDemo("Nested Nav Demo") { NestNavDemo() },
         ComposableDemo("Bottom Bar Nav Demo") { BottomBarNavDemo() },
         ComposableDemo("Navigation with Args") { NavWithArgsDemo() },
-        ComposableDemo("Navigation by DeepLink") { NavByDeepLinkDemo() }
+        ComposableDemo("Navigation by DeepLink") { NavByDeepLinkDemo() },
+        ComposableDemo("Navigation PopUpTo") { NavPopUpToDemo() },
+        ComposableDemo("Navigation SingleTop") { NavSingleTopDemo() }
     )
 )
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
index 9a71f30..a1b852e 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
@@ -17,12 +17,21 @@
 package androidx.navigation.compose
 
 import android.net.Uri
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Button
+import androidx.compose.material.Text
 import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.ContextAmbient
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
 import androidx.core.net.toUri
 import androidx.navigation.NavGraph
 import androidx.navigation.NavHostController
@@ -30,6 +39,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
+import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Rule
 import org.junit.Test
@@ -84,6 +94,58 @@
     }
 
     @Test
+    fun testNavigateOutsideStateChange() {
+        lateinit var navController: NavHostController
+        val text = "myButton"
+        var counter = 0
+        composeTestRule.setContent {
+            navController = rememberNavController()
+            var state by remember { mutableStateOf(0) }
+            Column(Modifier.fillMaxSize()) {
+                NavHost(navController, startDestination = "first") {
+                    composable("first") { }
+                    composable("second") { }
+                }
+                Button(
+                    >
+                        state++
+                        counter = state
+                    }
+                ) {
+                    Text(text)
+                }
+            }
+        }
+
+        assertWithMessage("Destination should be added to the graph")
+            .that("first" in navController.graph)
+            .isTrue()
+
+        composeTestRule.runOnIdle {
+            navController.navigate("second")
+        }
+
+        composeTestRule.runOnIdle {
+            assertWithMessage("second destination should be current")
+                .that(
+                    navController.currentDestination?.hasDeepLink(Uri.parse(createRoute("second")))
+                ).isTrue()
+        }
+
+        composeTestRule.onNodeWithText(text)
+            .performClick()
+
+        composeTestRule.runOnIdle {
+            // ensure our click listener was fired
+            assertThat(counter).isEqualTo(1)
+            assertWithMessage("second destination should be current")
+                .that(
+                    navController.currentDestination?.hasDeepLink(Uri.parse(createRoute("second")))
+                ).isTrue()
+        }
+    }
+
+    @Test
     fun testPop() {
         lateinit var navController: TestNavHostController
         composeTestRule.setContent {
@@ -113,8 +175,8 @@
         lateinit var state: MutableState<String>
         composeTestRule.setContent {
             state = remember { mutableStateOf("first") }
-
-            navController = TestNavHostController(ContextAmbient.current)
+            val context = ContextAmbient.current
+            navController = remember { TestNavHostController(context) }
 
             NavHost(navController, startDestination = state.value) {
                 test("first")
@@ -127,9 +189,9 @@
         }
 
         composeTestRule.runOnIdle {
-            assertWithMessage("Second destination should be current")
+            assertWithMessage("First destination should be current")
                 .that(
-                    navController.currentDestination?.hasDeepLink(createRoute("second").toUri())
+                    navController.currentDestination?.hasDeepLink(createRoute("first").toUri())
                 ).isTrue()
         }
     }
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
index e86802b..ed00cd4 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
@@ -42,6 +42,9 @@
  * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
  * the provided [navController].
  *
+ * The builder passed into this method is [remember]ed. This means that for this NavHost, the
+ * contents of the builder cannot be changed.
+ *
  * @sample androidx.navigation.compose.samples.BasicNav
  *
  * @param navController the navController for this host
@@ -70,6 +73,9 @@
  * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
  * the provided [navController].
  *
+ * The graph passed into this method is [remember]ed. This means that for this NavHost, the graph
+ * cannot be changed.
+ *
  * @param navController the navController for this host
  * @param graph the graph for this host
  */
@@ -79,6 +85,7 @@
     var context = ContextAmbient.current
     val lifecycleOwner = LifecycleOwnerAmbient.current
     val viewModelStore = ViewModelStoreOwnerAmbient.current.viewModelStore
+    val rememberedGraph = remember { graph }
 
     // on successful recompose we setup the navController with proper inputs
     // after the first time, this will only happen again if one of the inputs changes
@@ -98,8 +105,8 @@
         }
     }
 
-    onCommit(graph) {
-        navController.graph = graph
+    onCommit(rememberedGraph) {
+        navController.graph = rememberedGraph
     }
 
     val restorableStateHolder = rememberRestorableStateHolder<UUID>()
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
index eebd3a7..9e5dc62 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
@@ -473,6 +473,120 @@
     }
 
     /**
+     * Test that popping the last destination in a graph while navigating to a new
+     * destination in that graph keeps the graph around
+     */
+    @UiThreadTest
+    @Test
+    fun testLifecycleReplaceLastDestination() {
+        val navController = createNavController()
+        val navGraph = navController.navigatorProvider.navigation(
+            id = 1,
+            startDestination = R.id.nested
+        ) {
+            navigation(id = R.id.nested, startDestination = R.id.nested_test) {
+                test(R.id.nested_test)
+            }
+            test(R.id.second_test)
+        }
+        navController.graph = navGraph
+
+        val graphBackStackEntry = navController.getBackStackEntry(navGraph.id)
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val nestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
+        assertWithMessage("The nested graph should be resumed when its child is resumed")
+            .that(nestedGraphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+        assertWithMessage("The nested start destination should be resumed")
+            .that(nestedBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+
+        navController.navigate(
+            R.id.nested_test,
+            null,
+            navOptions {
+                popUpTo(R.id.nested_test) {
+                    inclusive = true
+                }
+            }
+        )
+
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertWithMessage("The nested graph should be resumed when its child is resumed")
+            .that(nestedGraphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertWithMessage("The nested start destination should be destroyed after being popped")
+            .that(nestedBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.DESTROYED)
+        val secondBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+        assertWithMessage("The new destination should be resumed")
+            .that(secondBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+    }
+
+    /**
+     * Test that popping the last destination in a graph while navigating correctly
+     * cleans up the previous navigation graph
+     */
+    @UiThreadTest
+    @Test
+    fun testLifecycleOrphanedGraph() {
+        val navController = createNavController()
+        val navGraph = navController.navigatorProvider.navigation(
+            id = 1,
+            startDestination = R.id.nested
+        ) {
+            navigation(id = R.id.nested, startDestination = R.id.nested_test) {
+                test(R.id.nested_test)
+            }
+            test(R.id.second_test)
+        }
+        navController.graph = navGraph
+
+        val graphBackStackEntry = navController.getBackStackEntry(navGraph.id)
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val nestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
+        assertWithMessage("The nested graph should be resumed when its child is resumed")
+            .that(nestedGraphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+        assertWithMessage("The nested start destination should be resumed")
+            .that(nestedBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+
+        navController.navigate(
+            R.id.second_test,
+            null,
+            navOptions {
+                popUpTo(R.id.nested_test) {
+                    inclusive = true
+                }
+            }
+        )
+
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertWithMessage("The nested graph should be destroyed when its children are destroyed")
+            .that(nestedGraphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.DESTROYED)
+        assertWithMessage("The nested start destination should be destroyed after being popped")
+            .that(nestedBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.DESTROYED)
+        val secondBackStackEntry = navController.getBackStackEntry(R.id.second_test)
+        assertWithMessage("The new destination should be resumed")
+            .that(secondBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+    }
+
+    /**
      * Test that navigating to a new instance of a graph leaves the previous instance in its
      * current state.
      */
@@ -546,6 +660,132 @@
     }
 
     /**
+     * Test that navigating to a new instance of a graph back to back with its previous
+     * instance creates a brand new graph instance
+     */
+    @UiThreadTest
+    @Test
+    fun testLifecycleNestedRepeatedBackToBack() {
+        val navController = createNavController()
+        val navGraph = navController.navigatorProvider.navigation(
+            id = 1,
+            startDestination = R.id.nested
+        ) {
+            navigation(id = R.id.nested, startDestination = R.id.nested_test) {
+                test(R.id.nested_test)
+            }
+            test(R.id.second_test)
+        }
+        navController.graph = navGraph
+
+        val graphBackStackEntry = navController.getBackStackEntry(navGraph.id)
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val nestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
+        assertWithMessage("The nested graph should be resumed when its child is resumed")
+            .that(nestedGraphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+        assertWithMessage("The nested start destination should be resumed")
+            .that(nestedBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+
+        // Navigate to a new instance of the graph, creating another copy
+        navController.navigate(navGraph.id)
+
+        assertWithMessage("The original parent graph should move to created")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.CREATED)
+        assertWithMessage("The original nested graph should move to created")
+            .that(nestedGraphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.CREATED)
+        assertWithMessage("The original nested start destination should move to created")
+            .that(nestedBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.CREATED)
+        val newGraphBackStackEntry = navController.getBackStackEntry(navGraph.id)
+        assertWithMessage("The new parent graph should be resumed when its child is resumed")
+            .that(newGraphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val newNestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
+        assertWithMessage("The new nested graph should be resumed when its child is resumed")
+            .that(newNestedGraphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val newNestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+        assertWithMessage("The new nested start destination should be resumed")
+            .that(newNestedBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+    }
+
+    /**
+     * Test that navigating to a new instance of a graph back to back with popping the
+     * last destination from the previous instance of the graph correctly cleans up
+     * the orphaned graph and creates a new graph instance.
+     */
+    @UiThreadTest
+    @Test
+    fun testLifecycleNestedRepeatedBackToBackWithOrphanedGraph() {
+        val navController = createNavController()
+        val navGraph = navController.navigatorProvider.navigation(
+            id = 1,
+            startDestination = R.id.nested
+        ) {
+            navigation(id = R.id.nested, startDestination = R.id.nested_test) {
+                test(R.id.nested_test)
+            }
+            test(R.id.second_test)
+        }
+        navController.graph = navGraph
+
+        val graphBackStackEntry = navController.getBackStackEntry(navGraph.id)
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val nestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
+        assertWithMessage("The nested graph should be resumed when its child is resumed")
+            .that(nestedGraphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+        assertWithMessage("The nested start destination should be resumed")
+            .that(nestedBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+
+        // Navigate to a new instance of the graph, creating another copy
+        // while popping the last destination from the previous graph
+        navController.navigate(
+            navGraph.id,
+            null,
+            navOptions {
+                popUpTo(R.id.nested_test) {
+                    inclusive = true
+                }
+            }
+        )
+
+        assertWithMessage("The parent graph should be destroyed when its children are destroyed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.DESTROYED)
+        assertWithMessage("The nested graph should be destroyed when its children are destroyed")
+            .that(nestedGraphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.DESTROYED)
+        assertWithMessage("The nested start destination should be destroyed after being popped")
+            .that(nestedBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.DESTROYED)
+        val newGraphBackStackEntry = navController.getBackStackEntry(navGraph.id)
+        assertWithMessage("The new parent graph should be resumed when its child is resumed")
+            .that(newGraphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val newNestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
+        assertWithMessage("The new nested graph should be resumed when its child is resumed")
+            .that(newNestedGraphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val newNestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
+        assertWithMessage("The new nested start destination should be resumed")
+            .that(newNestedBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+    }
+
+    /**
      * Test that navigating to a new instance of a graph via a deep link to a FloatingWindow
      * destination leaves the previous instance in its current state.
      */
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
index 6fe9305..beaa21a 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
@@ -1056,6 +1056,50 @@
 
     @UiThreadTest
     @Test
+    fun testNavigateOptionSingleTopReplaceWithDefaultArgs() {
+        val navController = createNavController()
+        navController.setGraph(R.navigation.nav_simple)
+        navController.navigate(R.id.start_test_with_default_arg)
+        assertThat(navController.currentDestination?.id ?: 0)
+            .isEqualTo(R.id.start_test_with_default_arg)
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        assertThat(navigator.backStack.size).isEqualTo(2)
+        assertThat(navigator.current.second).isNotNull()
+        assertThat(navigator.current.second?.getBoolean("defaultArg", false)).isTrue()
+
+        val args = Bundle()
+        val testKey = "testKey"
+        val testValue = "testValue"
+        args.putString(testKey, testValue)
+
+        var destinationListenerExecuted = false
+
+        navController.navigate(
+            R.id.start_test_with_default_arg, args,
+            navOptions {
+                launchSingleTop = true
+            }
+        )
+
+        navController.addOnDestinationChangedListener { _, destination, arguments ->
+            destinationListenerExecuted = true
+            assertThat(destination.id).isEqualTo(R.id.start_test_with_default_arg)
+            assertThat(arguments?.getString(testKey)).isEqualTo(testValue)
+            assertThat(arguments?.getBoolean("defaultArg", false)).isTrue()
+        }
+
+        assertThat(navController.currentDestination?.id ?: 0)
+            .isEqualTo(R.id.start_test_with_default_arg)
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        val returnedArgs = navigator.current.second
+        assertThat(returnedArgs?.getString(testKey)).isEqualTo(testValue)
+        assertThat(returnedArgs?.getBoolean("defaultArg", false)).isTrue()
+        assertThat(destinationListenerExecuted).isTrue()
+    }
+
+    @UiThreadTest
+    @Test
     fun testNavigateOptionSingleTopNewArgsIgnore() {
         val navController = createNavController()
         navController.setGraph(R.navigation.nav_simple)
diff --git a/navigation/navigation-runtime/src/androidTest/res/navigation/nav_simple.xml b/navigation/navigation-runtime/src/androidTest/res/navigation/nav_simple.xml
index e276c1c..fe966c1 100644
--- a/navigation/navigation-runtime/src/androidTest/res/navigation/nav_simple.xml
+++ b/navigation/navigation-runtime/src/androidTest/res/navigation/nav_simple.xml
@@ -24,6 +24,11 @@
         <action android:id="@+id/second" app:destination="@+id/second_test" />
     </test>
 
+    <test android:id="@+id/start_test_with_default_arg">
+        <argument android:name="defaultArg" android:defaultValue="true" />
+        <action android:id="@+id/second" app:destination="@+id/second_test" />
+    </test>
+
     <test android:id="@+id/second_test">
         <argument android:name="arg2" app:argType="string" />
         <action android:id="@+id/self" app:destination="@+id/second_test"
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.java b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.java
index 9031ac4..d327202 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.java
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.java
@@ -1085,16 +1085,33 @@
                     // Keep popping
                 }
             }
-            // The mGraph should always be on the back stack after you navigate()
-            if (mBackStack.isEmpty()) {
-                NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
-                        mLifecycleOwner, mViewModel);
-                mBackStack.add(entry);
-            }
-            // Now ensure all intermediate NavGraphs are put on the back stack
-            // to ensure that global actions work.
+
+            // When you navigate() to a NavGraph, we need to ensure that a new instance
+            // is always created vs reusing an existing copy of that destination
             ArrayDeque<NavBackStackEntry> hierarchy = new ArrayDeque<>();
             NavDestination destination = newDest;
+            if (node instanceof NavGraph) {
+                do {
+                    NavGraph parent = destination.getParent();
+                    if (parent != null) {
+                        NavBackStackEntry entry = new NavBackStackEntry(mContext, parent,
+                                finalArgs, mLifecycleOwner, mViewModel);
+                        hierarchy.addFirst(entry);
+                        // Pop any orphaned copy of that navigation graph off the back stack
+                        if (!mBackStack.isEmpty()
+                                && mBackStack.getLast().getDestination() == parent) {
+                            popBackStackInternal(parent.getId(), true);
+                        }
+                    }
+                    destination = parent;
+                } while (destination != null && destination != node);
+            }
+
+            // Now collect the set of all intermediate NavGraphs that need to be put onto
+            // the back stack
+            destination = hierarchy.isEmpty()
+                    ? newDest
+                    : hierarchy.getFirst().getDestination();
             while (destination != null && findDestination(destination.getId()) == null) {
                 NavGraph parent = destination.getParent();
                 if (parent != null) {
@@ -1104,7 +1121,25 @@
                 }
                 destination = parent;
             }
+            NavDestination overlappingDestination = hierarchy.isEmpty()
+                    ? newDest
+                    : hierarchy.getLast().getDestination();
+            // Pop any orphaned navigation graphs that don't connect to the new destinations
+            //noinspection StatementWithEmptyBody
+            while (!mBackStack.isEmpty()
+                    && mBackStack.getLast().getDestination() instanceof NavGraph
+                    && ((NavGraph) mBackStack.getLast().getDestination()).findNode(
+                            overlappingDestination.getId(), false) == null
+                    && popBackStackInternal(mBackStack.getLast().getDestination().getId(), true)) {
+                // Keep popping
+            }
             mBackStack.addAll(hierarchy);
+            // The mGraph should always be on the back stack after you navigate()
+            if (mBackStack.isEmpty() || mBackStack.getFirst().getDestination() != mGraph) {
+                NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
+                        mLifecycleOwner, mViewModel);
+                mBackStack.addFirst(entry);
+            }
             // And finally, add the new destination with its default args
             NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
                     newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
@@ -1113,7 +1148,7 @@
             launchSingleTop = true;
             NavBackStackEntry singleTopBackStackEntry = mBackStack.peekLast();
             if (singleTopBackStackEntry != null) {
-                singleTopBackStackEntry.replaceArguments(args);
+                singleTopBackStackEntry.replaceArguments(finalArgs);
             }
         }
         updateOnBackPressedCallbackEnabled();
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
index 6998bfe..1bae59f 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
@@ -801,6 +801,9 @@
                                         override fun beginTransaction() {
                                             throw RuntimeException("Error beginning transaction.")
                                         }
+                                        override fun beginTransactionNonExclusive() {
+                                            throw RuntimeException("Error beginning transaction.")
+                                        }
                                     }
                                 }
                             }
diff --git a/settings.gradle b/settings.gradle
index d8cc901..a1846dc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -148,12 +148,12 @@
 includeProject(":benchmark:benchmark-gradle-plugin", "benchmark/gradle-plugin", [BuildType.MAIN])
 includeProject(":benchmark:benchmark-junit4", "benchmark/junit4")
 includeProject(":benchmark:benchmark-macro", "benchmark/macro", [BuildType.MAIN])
-includeProject(":benchmark:benchmark-macro-runtime", "benchmark/benchmark-macro-runtime", [BuildType.MAIN])
+includeProject(":benchmark:benchmark-macro-runtime", "benchmark/benchmark-macro-runtime", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":benchmark:benchmark-perfetto", "benchmark/perfetto", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
 includeProject(":benchmark:benchmark-simple-macro-benchmark", "benchmark/benchmark-simple-macro-benchmark", [BuildType.MAIN])
-includeProject(":benchmark:benchmark-perfetto", "benchmark/perfetto", [BuildType.MAIN])
 includeProject(":benchmark:integration-tests:dry-run-benchmark", "benchmark/integration-tests/dry-run-benchmark", [BuildType.MAIN])
 includeProject(":benchmark:integration-tests:startup-benchmark", "benchmark/integration-tests/startup-benchmark", [BuildType.MAIN])
-includeProject(":benchmark:integration-tests:benchmark-simple-macro-benchmark-target", "benchmark/integration-tests/benchmark-simple-macro-benchmark-target", [BuildType.MAIN])
+includeProject(":benchmark:integration-tests:benchmark-simple-macro-benchmark-target", "benchmark/integration-tests/benchmark-simple-macro-benchmark-target", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":biometric:biometric", "biometric/biometric", [BuildType.MAIN])
 includeProject(":biometric:integration-tests:testapp", "biometric/integration-tests/testapp", [BuildType.MAIN])
 includeProject(":browser:browser", "browser/browser", [BuildType.MAIN])
@@ -464,7 +464,7 @@
 includeProject(":swiperefreshlayout:swiperefreshlayout", "swiperefreshlayout/swiperefreshlayout", [BuildType.MAIN])
 includeProject(":test-screenshot", "test/screenshot")
 includeProject(":test-screenshot-proto", "test/screenshot/proto", [BuildType.MAIN])
-includeProject(":text:text", "text/text")
+includeProject(":text:text", "text/text", [BuildType.COMPOSE])
 includeProject(":textclassifier:integration-tests:testapp", "textclassifier/integration-tests/testapp", [BuildType.MAIN])
 includeProject(":textclassifier:textclassifier", "textclassifier/textclassifier", [BuildType.MAIN])
 includeProject(":tracing:tracing", "tracing/tracing")
diff --git a/ui/ui-tooling/api/current.txt b/ui/ui-tooling/api/current.txt
index 3a99d46..e04894b 100644
--- a/ui/ui-tooling/api/current.txt
+++ b/ui/ui-tooling/api/current.txt
@@ -113,7 +113,7 @@
     method public String getName();
     method public int getOffset();
     method public int getPackageHash();
-    method public java.util.List<androidx.ui.tooling.inspector.NodeParameter> getParameters();
+    method public java.util.List<androidx.ui.tooling.inspector.RawParameter> getParameters();
     method public int getTop();
     method public int getWidth();
     property public final java.util.List<androidx.ui.tooling.inspector.InspectorNode> children;
@@ -126,7 +126,7 @@
     property public final String name;
     property public final int offset;
     property public final int packageHash;
-    property public final java.util.List<androidx.ui.tooling.inspector.NodeParameter> parameters;
+    property public final java.util.List<androidx.ui.tooling.inspector.RawParameter> parameters;
     property public final int top;
     property public final int width;
   }
@@ -134,6 +134,8 @@
   public final class LayoutInspectorTree {
     ctor public LayoutInspectorTree();
     method public java.util.List<androidx.ui.tooling.inspector.InspectorNode> convert(android.view.View view);
+    method public java.util.List<androidx.ui.tooling.inspector.NodeParameter> convertParameters(androidx.ui.tooling.inspector.InspectorNode node);
+    method public void resetGeneratedId();
   }
 
   public final class LayoutInspectorTreeKt {
@@ -167,6 +169,14 @@
     enum_constant public static final androidx.ui.tooling.inspector.ParameterType String;
   }
 
+  public final class RawParameter {
+    ctor public RawParameter(String name, Object? value);
+    method public String getName();
+    method public Object? getValue();
+    property public final String name;
+    property public final Object? value;
+  }
+
 }
 
 package androidx.ui.tooling.preview {
diff --git a/ui/ui-tooling/api/public_plus_experimental_current.txt b/ui/ui-tooling/api/public_plus_experimental_current.txt
index 3a99d46..e04894b 100644
--- a/ui/ui-tooling/api/public_plus_experimental_current.txt
+++ b/ui/ui-tooling/api/public_plus_experimental_current.txt
@@ -113,7 +113,7 @@
     method public String getName();
     method public int getOffset();
     method public int getPackageHash();
-    method public java.util.List<androidx.ui.tooling.inspector.NodeParameter> getParameters();
+    method public java.util.List<androidx.ui.tooling.inspector.RawParameter> getParameters();
     method public int getTop();
     method public int getWidth();
     property public final java.util.List<androidx.ui.tooling.inspector.InspectorNode> children;
@@ -126,7 +126,7 @@
     property public final String name;
     property public final int offset;
     property public final int packageHash;
-    property public final java.util.List<androidx.ui.tooling.inspector.NodeParameter> parameters;
+    property public final java.util.List<androidx.ui.tooling.inspector.RawParameter> parameters;
     property public final int top;
     property public final int width;
   }
@@ -134,6 +134,8 @@
   public final class LayoutInspectorTree {
     ctor public LayoutInspectorTree();
     method public java.util.List<androidx.ui.tooling.inspector.InspectorNode> convert(android.view.View view);
+    method public java.util.List<androidx.ui.tooling.inspector.NodeParameter> convertParameters(androidx.ui.tooling.inspector.InspectorNode node);
+    method public void resetGeneratedId();
   }
 
   public final class LayoutInspectorTreeKt {
@@ -167,6 +169,14 @@
     enum_constant public static final androidx.ui.tooling.inspector.ParameterType String;
   }
 
+  public final class RawParameter {
+    ctor public RawParameter(String name, Object? value);
+    method public String getName();
+    method public Object? getValue();
+    property public final String name;
+    property public final Object? value;
+  }
+
 }
 
 package androidx.ui.tooling.preview {
diff --git a/ui/ui-tooling/api/restricted_current.txt b/ui/ui-tooling/api/restricted_current.txt
index 3a99d46..e04894b 100644
--- a/ui/ui-tooling/api/restricted_current.txt
+++ b/ui/ui-tooling/api/restricted_current.txt
@@ -113,7 +113,7 @@
     method public String getName();
     method public int getOffset();
     method public int getPackageHash();
-    method public java.util.List<androidx.ui.tooling.inspector.NodeParameter> getParameters();
+    method public java.util.List<androidx.ui.tooling.inspector.RawParameter> getParameters();
     method public int getTop();
     method public int getWidth();
     property public final java.util.List<androidx.ui.tooling.inspector.InspectorNode> children;
@@ -126,7 +126,7 @@
     property public final String name;
     property public final int offset;
     property public final int packageHash;
-    property public final java.util.List<androidx.ui.tooling.inspector.NodeParameter> parameters;
+    property public final java.util.List<androidx.ui.tooling.inspector.RawParameter> parameters;
     property public final int top;
     property public final int width;
   }
@@ -134,6 +134,8 @@
   public final class LayoutInspectorTree {
     ctor public LayoutInspectorTree();
     method public java.util.List<androidx.ui.tooling.inspector.InspectorNode> convert(android.view.View view);
+    method public java.util.List<androidx.ui.tooling.inspector.NodeParameter> convertParameters(androidx.ui.tooling.inspector.InspectorNode node);
+    method public void resetGeneratedId();
   }
 
   public final class LayoutInspectorTreeKt {
@@ -167,6 +169,14 @@
     enum_constant public static final androidx.ui.tooling.inspector.ParameterType String;
   }
 
+  public final class RawParameter {
+    ctor public RawParameter(String name, Object? value);
+    method public String getName();
+    method public Object? getValue();
+    property public final String name;
+    property public final Object? value;
+  }
+
 }
 
 package androidx.ui.tooling.preview {
diff --git a/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/LayoutInspectorTreeTest.kt b/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/LayoutInspectorTreeTest.kt
index 48ad3aa..4fbfa86 100644
--- a/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/LayoutInspectorTreeTest.kt
+++ b/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/LayoutInspectorTreeTest.kt
@@ -93,9 +93,9 @@
         dumpSlotTableSet(slotTableRecord)
         val builder = LayoutInspectorTree()
         val nodes = builder.convert(view)
-        dumpNodes(nodes)
+        dumpNodes(nodes, builder)
 
-        validate(nodes, checkParameters = true) {
+        validate(nodes, builder, checkParameters = true) {
             node(
                 name = "Box",
                 fileName = "Box.kt",
@@ -404,10 +404,10 @@
         dumpSlotTableSet(slotTableRecord)
         val builder = LayoutInspectorTree()
         val nodes = builder.convert(view)
-        dumpNodes(nodes)
+        dumpNodes(nodes, builder)
 
         if (DEBUG) {
-            validate(nodes, checkParameters = false) {
+            validate(nodes, builder, checkParameters = false) {
                 node("Box", children = listOf("ModalDrawerLayout"))
                 node("ModalDrawerLayout", children = listOf("WithConstraints"))
                 node("WithConstraints", children = listOf("SubcomposeLayout"))
@@ -484,18 +484,20 @@
 
     private fun validate(
         result: List<InspectorNode>,
+        builder: LayoutInspectorTree,
         checkParameters: Boolean,
         block: TreeValidationReceiver.() -> Unit = {}
     ) {
         val nodes = result.flatMap { flatten(it) }.iterator()
-        val tree = TreeValidationReceiver(nodes, density, checkParameters)
+        val tree = TreeValidationReceiver(nodes, density, checkParameters, builder)
         tree.block()
     }
 
     private class TreeValidationReceiver(
         val nodeIterator: Iterator<InspectorNode>,
         val density: Density,
-        val checkParameters: Boolean
+        val checkParameters: Boolean,
+        val builder: LayoutInspectorTree
     ) {
         fun node(
             name: String,
@@ -534,11 +536,12 @@
             }
 
             if (checkParameters) {
-                val params = ParameterValidationReceiver(node.parameters.listIterator())
-                params.block()
-                if (params.parameterIterator.hasNext()) {
+                val params = builder.convertParameters(node)
+                val receiver = ParameterValidationReceiver(params.listIterator())
+                receiver.block()
+                if (receiver.parameterIterator.hasNext()) {
                     val elementNames = mutableListOf<String>()
-                    params.parameterIterator.forEachRemaining { elementNames.add(it.name) }
+                    receiver.parameterIterator.forEachRemaining { elementNames.add(it.name) }
                     error("$name: has more parameters like: ${elementNames.joinToString()}")
                 }
             }
@@ -549,7 +552,7 @@
         listOf(node).plus(node.children.flatMap { flatten(it) })
 
     // region DEBUG print methods
-    private fun dumpNodes(nodes: List<InspectorNode>) {
+    private fun dumpNodes(nodes: List<InspectorNode>, builder: LayoutInspectorTree) {
         @Suppress("ConstantConditionIf")
         if (!DEBUG) {
             return
@@ -559,7 +562,7 @@
         nodes.forEach { dumpNode(it, indent = 0) }
         println()
         println("=================== validate statements ==========================")
-        nodes.forEach { generateValidate(it) }
+        nodes.forEach { generateValidate(it, builder) }
     }
 
     private fun dumpNode(node: InspectorNode, indent: Int) {
@@ -571,7 +574,7 @@
         node.children.forEach { dumpNode(it, indent + 1) }
     }
 
-    private fun generateValidate(node: InspectorNode) {
+    private fun generateValidate(node: InspectorNode, builder: LayoutInspectorTree) {
         with(density) {
             val left = round(node.left.toDp())
             val top = round(node.top.toDp())
@@ -599,10 +602,10 @@
         println()
         print(")")
         if (node.parameters.isNotEmpty()) {
-            generateParameters(node.parameters, 0)
+            generateParameters(builder.convertParameters(node), 0)
         }
         println()
-        node.children.forEach { generateValidate(it) }
+        node.children.forEach { generateValidate(it, builder) }
     }
 
     private fun generateParameters(parameters: List<NodeParameter>, indent: Int) {
diff --git a/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/ParameterFactoryTest.kt b/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/ParameterFactoryTest.kt
index 02ce967..200a1ad 100644
--- a/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/ParameterFactoryTest.kt
+++ b/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/inspector/ParameterFactoryTest.kt
@@ -75,7 +75,6 @@
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.After
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -83,15 +82,16 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class ParameterFactoryTest {
-    private val node = MutableInspectorNode()
     private val factory = ParameterFactory(InlineClassConverter())
     private val api = android.os.Build.VERSION.SDK_INT
+    private val node = MutableInspectorNode().apply {
+        width = 1000
+        height = 500
+    }.build()
 
     @Before
     fun before() {
         factory.density = Density(2.0f)
-        node.width = 1000
-        node.height = 500
         isDebugInspectorInfoEnabled = true
     }
 
@@ -101,7 +101,6 @@
     }
 
     @Test
-    @Ignore("b/172466485")
     fun testAbsoluteAlignment() {
         assertThat(lookup(AbsoluteAlignment.TopLeft))
             .isEqualTo(ParameterType.String to "TopLeft")
diff --git a/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/InspectorNode.kt b/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/InspectorNode.kt
index f933d53..d495e31 100644
--- a/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/InspectorNode.kt
+++ b/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/InspectorNode.kt
@@ -89,7 +89,7 @@
     /**
      * The parameters of this Composable.
      */
-    val parameters: List<NodeParameter>,
+    val parameters: List<RawParameter>,
 
     /**
      * The children nodes of this Composable.
@@ -98,6 +98,11 @@
 )
 
 /**
+ * Parameter definition with a raw value reference.
+ */
+class RawParameter(val name: String, val value: Any?)
+
+/**
  * Mutable version of [InspectorNode].
  */
 @ExperimentalLayoutNodeApi
@@ -114,7 +119,7 @@
     var top = 0
     var width = 0
     var height = 0
-    val parameters = mutableListOf<NodeParameter>()
+    val parameters = mutableListOf<RawParameter>()
     val children = mutableListOf<InspectorNode>()
 
     fun reset() {
diff --git a/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/LayoutInspectorTree.kt b/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/LayoutInspectorTree.kt
index 792b150..a6dc0eb 100644
--- a/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/LayoutInspectorTree.kt
+++ b/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/LayoutInspectorTree.kt
@@ -89,10 +89,23 @@
         return result
     }
 
+    /**
+     * Converts the [RawParameter]s of the [node] into displayable parameters.
+     */
+    fun convertParameters(node: InspectorNode): List<NodeParameter> {
+        return node.parameters.mapNotNull { parameterFactory.create(node, it.name, it.value) }
+    }
+
+    /**
+     * Reset the generated id. Nodes are assigned an id if there isn't a layout node id present.
+     */
+    fun resetGeneratedId() {
+        generatedId = -1L
+    }
+
     private fun clear() {
         cache.clear()
         inlineClassConverter.clear()
-        generatedId = -1L
         claimedNodes.clear()
         treeMap.clear()
         ownerMap.clear()
@@ -298,8 +311,8 @@
         parameters.forEach { addParameter(it, node) }
 
     private fun addParameter(parameter: ParameterInformation, node: MutableInspectorNode) {
-        val castedValue = castValue(parameter) ?: return
-        parameterFactory.create(node, parameter.name, castedValue)?.let { node.parameters.add(it) }
+        val castedValue = castValue(parameter)
+        node.parameters.add(RawParameter(parameter.name, castedValue))
     }
 
     private fun castValue(parameter: ParameterInformation): Any? {
diff --git a/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/ParameterFactory.kt b/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/ParameterFactory.kt
index 2d0a0347..cc12a73 100644
--- a/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/ParameterFactory.kt
+++ b/ui/ui-tooling/src/main/java/androidx/ui/tooling/inspector/ParameterFactory.kt
@@ -17,7 +17,9 @@
 package androidx.ui.tooling.inspector
 
 import android.util.Log
+import android.view.View
 import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.ui.AbsoluteAlignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
@@ -97,6 +99,10 @@
         valueLookup[Color.Unspecified] = "Unspecified"
         valuesLoaded.add(Enum::class.java)
         valuesLoaded.add(Any::class.java)
+
+        // AbsoluteAlignment is not found from an instance of BiasAbsoluteAlignment,
+        // because Alignment has no file level class.
+        loadConstantsFromEnclosedClasses(AbsoluteAlignment::class.java)
     }
 
     /**
@@ -105,7 +111,7 @@
      * Attempt to convert the value to a user readable value.
      * For now: return null when a conversion is not possible/found.
      */
-    fun create(node: MutableInspectorNode, name: String, value: Any?): NodeParameter? {
+    fun create(node: InspectorNode, name: String, value: Any?): NodeParameter? {
         val creator = creatorCache ?: ParameterCreator()
         try {
             return creator.create(node, name, value)
@@ -233,10 +239,10 @@
      * Convenience class for building [NodeParameter]s.
      */
     private inner class ParameterCreator {
-        private var node: MutableInspectorNode? = null
+        private var node: InspectorNode? = null
         private var recursions = 0
 
-        fun create(node: MutableInspectorNode, name: String, value: Any?): NodeParameter? = try {
+        fun create(node: InspectorNode, name: String, value: Any?): NodeParameter? = try {
             this.node = node
             recursions = 0
             create(name, value)
@@ -277,6 +283,7 @@
                     is String -> NodeParameter(name, ParameterType.String, value)
                     is TextUnit -> createFromTextUnit(name, value)
                     is VectorAsset -> createFromVectorAssert(name, value)
+                    is View -> NodeParameter(name, ParameterType.String, value.javaClass.simpleName)
                     else -> createFromKotlinReflection(name, value)
                 }
             } finally {
diff --git a/wear/wear-complications-data/api/current.txt b/wear/wear-complications-data/api/current.txt
index 81279f6..5e6049b 100644
--- a/wear/wear-complications-data/api/current.txt
+++ b/wear/wear-complications-data/api/current.txt
@@ -538,6 +538,7 @@
   }
 
   public final class TimeReference {
+    method public static androidx.wear.complications.data.TimeReference between(long startDateTimeMillis, long endDateTimeMillis);
     method public static androidx.wear.complications.data.TimeReference ending(long dateTimeMillis);
     method public long getEndDateTimeMillis();
     method public long getStartDateTimeMillis();
@@ -550,6 +551,7 @@
   }
 
   public static final class TimeReference.Companion {
+    method public androidx.wear.complications.data.TimeReference between(long startDateTimeMillis, long endDateTimeMillis);
     method public androidx.wear.complications.data.TimeReference ending(long dateTimeMillis);
     method public androidx.wear.complications.data.TimeReference starting(long dateTimeMillis);
   }
diff --git a/wear/wear-complications-data/api/public_plus_experimental_current.txt b/wear/wear-complications-data/api/public_plus_experimental_current.txt
index a99db16f..97d4589 100644
--- a/wear/wear-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/wear-complications-data/api/public_plus_experimental_current.txt
@@ -549,6 +549,7 @@
   }
 
   public final class TimeReference {
+    method public static androidx.wear.complications.data.TimeReference between(long startDateTimeMillis, long endDateTimeMillis);
     method public static androidx.wear.complications.data.TimeReference ending(long dateTimeMillis);
     method public long getEndDateTimeMillis();
     method public long getStartDateTimeMillis();
@@ -561,6 +562,7 @@
   }
 
   public static final class TimeReference.Companion {
+    method public androidx.wear.complications.data.TimeReference between(long startDateTimeMillis, long endDateTimeMillis);
     method public androidx.wear.complications.data.TimeReference ending(long dateTimeMillis);
     method public androidx.wear.complications.data.TimeReference starting(long dateTimeMillis);
   }
diff --git a/wear/wear-complications-data/api/restricted_current.txt b/wear/wear-complications-data/api/restricted_current.txt
index 767e483..d61fd21 100644
--- a/wear/wear-complications-data/api/restricted_current.txt
+++ b/wear/wear-complications-data/api/restricted_current.txt
@@ -591,6 +591,7 @@
   }
 
   public final class TimeReference {
+    method public static androidx.wear.complications.data.TimeReference between(long startDateTimeMillis, long endDateTimeMillis);
     method public static androidx.wear.complications.data.TimeReference ending(long dateTimeMillis);
     method public long getEndDateTimeMillis();
     method public long getStartDateTimeMillis();
@@ -603,6 +604,7 @@
   }
 
   public static final class TimeReference.Companion {
+    method public androidx.wear.complications.data.TimeReference between(long startDateTimeMillis, long endDateTimeMillis);
     method public androidx.wear.complications.data.TimeReference ending(long dateTimeMillis);
     method public androidx.wear.complications.data.TimeReference starting(long dateTimeMillis);
   }
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Time.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Time.kt
index d08a384..f4f99e7 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Time.kt
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Time.kt
@@ -49,10 +49,10 @@
 }
 
 /**
- * Expresses the reference point for a time difference.
+ * Expresses a reference point or range for a time difference.
  *
- * It defines one of [endDateTimeMillis] or [startDateTimeMillis] to express the corresponding
- * time differences relative before or after the givene point in time.
+ * It defines [endDateTimeMillis] and/or [startDateTimeMillis] to express the corresponding
+ * time differences relative to before, between or after the given point(s) in time.
  */
 public class TimeReference internal constructor(
     public val endDateTimeMillis: Long,
@@ -76,5 +76,12 @@
         @JvmStatic
         public fun starting(dateTimeMillis: Long): TimeReference =
             TimeReference(NONE, dateTimeMillis)
+
+        /**
+         * Creates a [TimeReference] for the time difference between [startDateTimeMillis] and [endDateTimeMillis].
+         */
+        @JvmStatic
+        public fun between(startDateTimeMillis: Long, endDateTimeMillis: Long): TimeReference =
+            TimeReference(endDateTimeMillis, startDateTimeMillis)
     }
 }
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index 76987d6..2741333 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -50,11 +50,18 @@
   public final class WindowManager {
     ctor public WindowManager(android.content.Context);
     ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
+    method public androidx.window.WindowMetrics getCurrentWindowMetrics();
+    method public androidx.window.WindowMetrics getMaximumWindowMetrics();
     method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
     method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
   }
 
+  public final class WindowMetrics {
+    ctor public WindowMetrics(android.graphics.Rect);
+    method public android.graphics.Rect getBounds();
+  }
+
 }
 
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index 76987d6..2741333 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -50,11 +50,18 @@
   public final class WindowManager {
     ctor public WindowManager(android.content.Context);
     ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
+    method public androidx.window.WindowMetrics getCurrentWindowMetrics();
+    method public androidx.window.WindowMetrics getMaximumWindowMetrics();
     method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
     method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
   }
 
+  public final class WindowMetrics {
+    ctor public WindowMetrics(android.graphics.Rect);
+    method public android.graphics.Rect getBounds();
+  }
+
 }
 
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index 76987d6..2741333 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -50,11 +50,18 @@
   public final class WindowManager {
     ctor public WindowManager(android.content.Context);
     ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
+    method public androidx.window.WindowMetrics getCurrentWindowMetrics();
+    method public androidx.window.WindowMetrics getMaximumWindowMetrics();
     method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
     method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
   }
 
+  public final class WindowMetrics {
+    ctor public WindowMetrics(android.graphics.Rect);
+    method public android.graphics.Rect getBounds();
+  }
+
 }
 
diff --git a/window/window/src/androidTest/java/androidx/window/TestWindowBoundsHelper.java b/window/window/src/androidTest/java/androidx/window/TestWindowBoundsHelper.java
index 90d3156..15a069e 100644
--- a/window/window/src/androidTest/java/androidx/window/TestWindowBoundsHelper.java
+++ b/window/window/src/androidTest/java/androidx/window/TestWindowBoundsHelper.java
@@ -33,6 +33,7 @@
 class TestWindowBoundsHelper extends WindowBoundsHelper {
     private Rect mGlobalOverriddenBounds;
     private final HashMap<Activity, Rect> mOverriddenBounds = new HashMap<>();
+    private final HashMap<Activity, Rect> mOverriddenMaximumBounds = new HashMap<>();
 
     /**
      * Overrides the bounds returned from this helper for the given context. Passing null {@code
@@ -46,6 +47,14 @@
     }
 
     /**
+     * Overrides the max bounds returned from this helper for the given context. Passing {@code
+     * null} {@code bounds} has the effect of clearing the bounds override.
+     */
+    void setMaximumBoundsForActivity(@NonNull Activity activity, @Nullable Rect bounds) {
+        mOverriddenMaximumBounds.put(activity, bounds);
+    }
+
+    /**
      * Overrides the bounds returned from this helper for all supplied contexts. Passing null
      * {@code bounds} has the effect of clearing the global override.
      */
@@ -68,6 +77,17 @@
         return super.computeCurrentWindowBounds(activity);
     }
 
+    @NonNull
+    @Override
+    Rect computeMaximumWindowBounds(Activity activity) {
+        Rect bounds = mOverriddenMaximumBounds.get(activity);
+        if (bounds != null) {
+            return bounds;
+        }
+
+        return super.computeMaximumWindowBounds(activity);
+    }
+
     /**
      * Clears any overrides set with {@link #setCurrentBounds(Rect)} or
      * {@link #setCurrentBoundsForActivity(Activity, Rect)}.
@@ -75,5 +95,6 @@
     void reset() {
         mGlobalOverriddenBounds = null;
         mOverriddenBounds.clear();
+        mOverriddenMaximumBounds.clear();
     }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java b/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java
index 93cfa19..cf6eab3 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
 
 import android.app.Activity;
 import android.graphics.Point;
@@ -45,7 +46,9 @@
             new ActivityScenarioRule<>(TestActivity.class);
 
     @Test
-    public void testGetCurrentWindowBounds_matchParentWindowSize_avoidCutouts() {
+    public void testGetCurrentWindowBounds_matchParentWindowSize_avoidCutouts_preR() {
+        assumePlatformBeforeR();
+
         testGetCurrentWindowBoundsMatchesRealDisplaySize(activity -> {
             assumeFalse(isInMultiWindowMode(activity));
 
@@ -61,7 +64,9 @@
     }
 
     @Test
-    public void testGetCurrentWindowBounds_fixedWindowSize_avoidCutouts() {
+    public void testGetCurrentWindowBounds_fixedWindowSize_avoidCutouts_preR() {
+        assumePlatformBeforeR();
+
         testGetCurrentWindowBoundsMatchesRealDisplaySize(activity -> {
             assumeFalse(isInMultiWindowMode(activity));
 
@@ -77,7 +82,9 @@
     }
 
     @Test
-    public void testGetCurrentWindowBounds_matchParentWindowSize_layoutBehindCutouts() {
+    public void testGetCurrentWindowBounds_matchParentWindowSize_layoutBehindCutouts_preR() {
+        assumePlatformBeforeR();
+
         testGetCurrentWindowBoundsMatchesRealDisplaySize(activity -> {
             assumeFalse(isInMultiWindowMode(activity));
 
@@ -93,7 +100,9 @@
     }
 
     @Test
-    public void testGetCurrentWindowBounds_fixedWindowSize_layoutBehindCutouts() {
+    public void testGetCurrentWindowBounds_fixedWindowSize_layoutBehindCutouts_preR() {
+        assumePlatformBeforeR();
+
         testGetCurrentWindowBoundsMatchesRealDisplaySize(activity -> {
             assumeFalse(isInMultiWindowMode(activity));
 
@@ -108,22 +117,137 @@
         });
     }
 
+    @Test
+    public void testGetCurrentWindowBounds_postR() {
+        assumePlatformROrAbove();
+
+        runActionsAcrossActivityLifecycle(activity -> { }, activity -> {
+            Rect bounds = WindowBoundsHelper.getInstance().computeCurrentWindowBounds(activity);
+            Rect windowMetricsBounds =
+                    activity.getWindowManager().getCurrentWindowMetrics().getBounds();
+            assertEquals(windowMetricsBounds, bounds);
+        });
+    }
+
+    @Test
+    public void testGetMaximumWindowBounds_matchParentWindowSize_avoidCutouts_preR() {
+        assumePlatformBeforeR();
+
+        testGetMaximumWindowBoundsMatchesRealDisplaySize(activity -> {
+            assumeFalse(isInMultiWindowMode(activity));
+
+            WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
+            lp.width = WindowManager.LayoutParams.MATCH_PARENT;
+            lp.height = WindowManager.LayoutParams.MATCH_PARENT;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                lp.layoutInDisplayCutoutMode =
+                        WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
+            }
+            activity.getWindow().setAttributes(lp);
+        });
+    }
+
+    @Test
+    public void testGetMaximumWindowBounds_fixedWindowSize_avoidCutouts_preR() {
+        assumePlatformBeforeR();
+
+        testGetMaximumWindowBoundsMatchesRealDisplaySize(activity -> {
+            assumeFalse(isInMultiWindowMode(activity));
+
+            WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
+            lp.width = 100;
+            lp.height = 100;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                lp.layoutInDisplayCutoutMode =
+                        WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
+            }
+            activity.getWindow().setAttributes(lp);
+        });
+    }
+
+    @Test
+    public void testGetMaximumWindowBounds_matchParentWindowSize_layoutBehindCutouts_preR() {
+        assumePlatformBeforeR();
+
+        testGetMaximumWindowBoundsMatchesRealDisplaySize(activity -> {
+            assumeFalse(isInMultiWindowMode(activity));
+
+            WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
+            lp.width = WindowManager.LayoutParams.MATCH_PARENT;
+            lp.height = WindowManager.LayoutParams.MATCH_PARENT;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                lp.layoutInDisplayCutoutMode =
+                        WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+            }
+            activity.getWindow().setAttributes(lp);
+        });
+    }
+
+    @Test
+    public void testGetMaximumWindowBounds_fixedWindowSize_layoutBehindCutouts_preR() {
+        assumePlatformBeforeR();
+
+        testGetMaximumWindowBoundsMatchesRealDisplaySize(activity -> {
+            assumeFalse(isInMultiWindowMode(activity));
+
+            WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
+            lp.width = 100;
+            lp.height = 100;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                lp.layoutInDisplayCutoutMode =
+                        WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+            }
+            activity.getWindow().setAttributes(lp);
+        });
+    }
+
+    @Test
+    public void testGetMaximumWindowBounds_postR() {
+        assumePlatformROrAbove();
+
+        runActionsAcrossActivityLifecycle(activity -> { }, activity -> {
+            Rect bounds = WindowBoundsHelper.getInstance().computeMaximumWindowBounds(activity);
+            Rect windowMetricsBounds =
+                    activity.getWindowManager().getMaximumWindowMetrics().getBounds();
+            assertEquals(windowMetricsBounds, bounds);
+        });
+    }
+
     private void testGetCurrentWindowBoundsMatchesRealDisplaySize(
             ActivityScenario.ActivityAction<TestActivity> initialAction) {
+        ActivityScenario.ActivityAction<TestActivity> assertWindowBoundsMatchesDisplayAction =
+                new AssertCurrentWindowBoundsEqualsRealDisplaySizeAction();
+        runActionsAcrossActivityLifecycle(initialAction, assertWindowBoundsMatchesDisplayAction);
+    }
+
+    private void testGetMaximumWindowBoundsMatchesRealDisplaySize(
+            ActivityScenario.ActivityAction<TestActivity> initialAction) {
+        ActivityScenario.ActivityAction<TestActivity> assertWindowBoundsMatchesDisplayAction =
+                new AssertMaximumWindowBoundsEqualsRealDisplaySizeAction();
+        runActionsAcrossActivityLifecycle(initialAction, assertWindowBoundsMatchesDisplayAction);
+    }
+
+    /**
+     * Creates and launches an activity performing the supplied actions at various points in the
+     * activity lifecycle.
+     *
+     * @param initialAction the action that will run once before the activity is created.
+     * @param verifyAction the action to run once after each change in activity lifecycle state.
+     */
+    private void runActionsAcrossActivityLifecycle(
+            ActivityScenario.ActivityAction<TestActivity> initialAction,
+            ActivityScenario.ActivityAction<TestActivity> verifyAction) {
         ActivityScenario<TestActivity> scenario = mActivityScenarioRule.getScenario();
         scenario.onActivity(initialAction);
 
-        ActivityScenario.ActivityAction<TestActivity> assertWindowBoundsAction =
-                new AssertWindowBoundsEqualsRealDisplaySizeAction();
-
         scenario.moveToState(Lifecycle.State.CREATED);
-        scenario.onActivity(assertWindowBoundsAction);
+        scenario.onActivity(verifyAction);
 
         scenario.moveToState(Lifecycle.State.STARTED);
-        scenario.onActivity(assertWindowBoundsAction);
+        scenario.onActivity(verifyAction);
 
         scenario.moveToState(Lifecycle.State.RESUMED);
-        scenario.onActivity(assertWindowBoundsAction);
+        scenario.onActivity(verifyAction);
     }
 
     private static boolean isInMultiWindowMode(Activity activity) {
@@ -133,7 +257,15 @@
         return false;
     }
 
-    private static final class AssertWindowBoundsEqualsRealDisplaySizeAction implements
+    private static void assumePlatformBeforeR() {
+        assumeTrue(Build.VERSION.SDK_INT < Build.VERSION_CODES.R);
+    }
+
+    private static void assumePlatformROrAbove() {
+        assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R);
+    }
+
+    private static final class AssertCurrentWindowBoundsEqualsRealDisplaySizeAction implements
             ActivityScenario.ActivityAction<TestActivity> {
         @Override
         public void perform(TestActivity activity) {
@@ -153,4 +285,25 @@
                     realDisplaySize.y, bounds.height());
         }
     }
+
+    private static final class AssertMaximumWindowBoundsEqualsRealDisplaySizeAction implements
+            ActivityScenario.ActivityAction<TestActivity> {
+        @Override
+        public void perform(TestActivity activity) {
+            Display display;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                display = activity.getDisplay();
+            } else {
+                display = activity.getWindowManager().getDefaultDisplay();
+            }
+
+            Point realDisplaySize = WindowBoundsHelper.getRealSizeForDisplay(display);
+
+            Rect bounds = WindowBoundsHelper.getInstance().computeMaximumWindowBounds(activity);
+            assertEquals("Window bounds width does not match real display width",
+                    realDisplaySize.x, bounds.width());
+            assertEquals("Window bounds height does not match real display height",
+                    realDisplaySize.y, bounds.height());
+        }
+    }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/WindowManagerTest.java b/window/window/src/androidTest/java/androidx/window/WindowManagerTest.java
index 957e9cb..dddc9ef 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowManagerTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowManagerTest.java
@@ -16,12 +16,15 @@
 
 package androidx.window;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
 import android.app.Activity;
 import android.content.ContextWrapper;
+import android.graphics.Rect;
 
 import androidx.core.util.Consumer;
 import androidx.test.core.app.ApplicationProvider;
@@ -30,6 +33,7 @@
 
 import com.google.common.util.concurrent.MoreExecutors;
 
+import org.junit.After;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -40,6 +44,11 @@
 @RunWith(AndroidJUnit4.class)
 public final class WindowManagerTest extends WindowTestBase {
 
+    @After
+    public void tearDown() {
+        WindowBoundsHelper.setForTesting(null);
+    }
+
     @Test
     public void testConstructor_activity() {
         new WindowManager(mock(Activity.class), mock(WindowBackend.class));
@@ -89,4 +98,36 @@
         wm.unregisterDeviceStateChangeCallback(consumer);
         verify(backend).unregisterDeviceStateChangeCallback(eq(consumer));
     }
+
+    @Test
+    public void testGetCurrentWindowMetrics() {
+        WindowBackend backend = mock(WindowBackend.class);
+        Activity activity = mock(Activity.class);
+        WindowManager wm = new WindowManager(activity, backend);
+
+        Rect bounds = new Rect(1, 2, 3, 4);
+        TestWindowBoundsHelper mWindowBoundsHelper = new TestWindowBoundsHelper();
+        mWindowBoundsHelper.setCurrentBoundsForActivity(activity, bounds);
+        WindowBoundsHelper.setForTesting(mWindowBoundsHelper);
+
+        WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();
+        assertNotNull(windowMetrics);
+        assertEquals(bounds, windowMetrics.getBounds());
+    }
+
+    @Test
+    public void testGetMaximumWindowMetrics() {
+        WindowBackend backend = mock(WindowBackend.class);
+        Activity activity = mock(Activity.class);
+        WindowManager wm = new WindowManager(activity, backend);
+
+        Rect bounds = new Rect(0, 2, 4, 5);
+        TestWindowBoundsHelper mWindowBoundsHelper = new TestWindowBoundsHelper();
+        mWindowBoundsHelper.setMaximumBoundsForActivity(activity, bounds);
+        WindowBoundsHelper.setForTesting(mWindowBoundsHelper);
+
+        WindowMetrics windowMetrics = wm.getMaximumWindowMetrics();
+        assertNotNull(windowMetrics);
+        assertEquals(bounds, windowMetrics.getBounds());
+    }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/WindowMetricsTest.java b/window/window/src/androidTest/java/androidx/window/WindowMetricsTest.java
new file mode 100644
index 0000000..126e31e
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/WindowMetricsTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.window;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.graphics.Rect;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link WindowMetrics} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class WindowMetricsTest {
+
+    @Test
+    public void testGetBounds() {
+        Rect bounds = new Rect(1, 2, 3, 4);
+        WindowMetrics windowMetrics = new WindowMetrics(bounds);
+        assertEquals(bounds, windowMetrics.getBounds());
+    }
+
+    @Test
+    public void testEquals_sameBounds() {
+        Rect bounds = new Rect(1, 2, 3, 4);
+        WindowMetrics windowMetrics0 = new WindowMetrics(bounds);
+        WindowMetrics windowMetrics1 = new WindowMetrics(bounds);
+
+        assertEquals(windowMetrics0, windowMetrics1);
+    }
+
+    @Test
+    public void testEquals_differentBounds() {
+        Rect bounds0 = new Rect(1, 2, 3, 4);
+        WindowMetrics windowMetrics0 = new WindowMetrics(bounds0);
+
+        Rect bounds1 = new Rect(6, 7, 8, 9);
+        WindowMetrics windowMetrics1 = new WindowMetrics(bounds1);
+
+        assertNotEquals(windowMetrics0, windowMetrics1);
+    }
+
+    @Test
+    public void testHashCode_matchesIfEqual() {
+        Rect bounds = new Rect(1, 2, 3, 4);
+        WindowMetrics windowMetrics0 = new WindowMetrics(bounds);
+        WindowMetrics windowMetrics1 = new WindowMetrics(bounds);
+
+        assertEquals(windowMetrics0.hashCode(), windowMetrics1.hashCode());
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/WindowBoundsHelper.java b/window/window/src/main/java/androidx/window/WindowBoundsHelper.java
index 882f30c..d2db428 100644
--- a/window/window/src/main/java/androidx/window/WindowBoundsHelper.java
+++ b/window/window/src/main/java/androidx/window/WindowBoundsHelper.java
@@ -104,14 +104,13 @@
      * <p>
      * Note: The value of this is based on the last windowing state reported to the client.
      *
+     * @see android.view.WindowManager#getCurrentWindowMetrics()
      * @see android.view.WindowMetrics#getBounds()
      */
     @NonNull
     Rect computeCurrentWindowBounds(Activity activity) {
         if (Build.VERSION.SDK_INT >= R) {
-            android.view.WindowManager platformWindowManager =
-                    activity.getSystemService(android.view.WindowManager.class);
-            return platformWindowManager.getCurrentWindowMetrics().getBounds();
+            return activity.getWindowManager().getCurrentWindowMetrics().getBounds();
         } else if (Build.VERSION.SDK_INT >= Q) {
             return computeWindowBoundsQ(activity);
         } else if (Build.VERSION.SDK_INT >= P) {
@@ -123,6 +122,27 @@
         }
     }
 
+    /**
+     * Computes the maximum size and position of the area the window can expect with
+     * {@link android.view.WindowManager.LayoutParams#MATCH_PARENT MATCH_PARENT} width and height
+     * and any combination of flags that would allow the window to extend behind display cutouts.
+     * <p>
+     * The value returned from this method will always match {@link Display#getRealSize(Point)} on
+     * {@link Build.VERSION_CODES#Q Android 10} and below.
+     *
+     * @see android.view.WindowManager#getMaximumWindowMetrics()
+     */
+    @NonNull
+    Rect computeMaximumWindowBounds(Activity activity) {
+        if (Build.VERSION.SDK_INT >= R) {
+            return activity.getWindowManager().getMaximumWindowMetrics().getBounds();
+        } else {
+            Display display = activity.getWindowManager().getDefaultDisplay();
+            Point displaySize = getRealSizeForDisplay(display);
+            return new Rect(0, 0, displaySize.x, displaySize.y);
+        }
+    }
+
     /** Computes the window bounds for {@link Build.VERSION_CODES#Q}. */
     @NonNull
     @RequiresApi(Q)
diff --git a/window/window/src/main/java/androidx/window/WindowManager.java b/window/window/src/main/java/androidx/window/WindowManager.java
index 9304d49..76c3633 100644
--- a/window/window/src/main/java/androidx/window/WindowManager.java
+++ b/window/window/src/main/java/androidx/window/WindowManager.java
@@ -19,6 +19,7 @@
 import android.app.Activity;
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.graphics.Rect;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -107,6 +108,57 @@
     }
 
     /**
+     * Returns the {@link WindowMetrics} according to the current system state.
+     * <p>
+     * The metrics describe the size of the area the window would occupy with
+     * {@link android.view.WindowManager.LayoutParams#MATCH_PARENT MATCH_PARENT} width and height
+     * and any combination of flags that would allow the window to extend behind display cutouts.
+     * <p>
+     * The value of this is based on the <b>current</b> windowing state of the system. For
+     * example, for activities in multi-window mode, the metrics returned are based on the
+     * current bounds that the user has selected for the {@link android.app.Activity Activity}'s
+     * window.
+     *
+     * @see #getMaximumWindowMetrics()
+     * @see android.view.WindowManager#getCurrentWindowMetrics()
+     */
+    @NonNull
+    public WindowMetrics getCurrentWindowMetrics() {
+        Activity activity = getActivityFromContext(mContext);
+        Rect currentBounds = WindowBoundsHelper.getInstance().computeCurrentWindowBounds(activity);
+        return new WindowMetrics(currentBounds);
+    }
+
+    /**
+     * Returns the largest {@link WindowMetrics} an app may expect in the current system state.
+     * <p>
+     * The metrics describe the size of the largest potential area the window might occupy with
+     * {@link android.view.WindowManager.LayoutParams#MATCH_PARENT MATCH_PARENT} width and height
+     * and any combination of flags that would allow the window to extend behind display cutouts.
+     * <p>
+     * The value of this is based on the largest <b>potential</b> windowing state of the system.
+     * For example, for activities in multi-window mode the metrics returned are based on what the
+     * bounds would be if the user expanded the window to cover the entire screen.
+     * <p>
+     * Note that this might still be smaller than the size of the physical display if certain
+     * areas of the display are not available to windows created for the associated {@link Context}.
+     * For example, devices with foldable displays that wrap around the enclosure may split the
+     * physical display into different regions, one for the front and one for the back, each acting
+     * as different logical displays. In this case {@link #getMaximumWindowMetrics()} would return
+     * the region describing the side of the device the associated {@link Context context's}
+     * window is placed.
+     *
+     * @see #getCurrentWindowMetrics()
+     * @see android.view.WindowManager#getMaximumWindowMetrics()
+     */
+    @NonNull
+    public WindowMetrics getMaximumWindowMetrics() {
+        Activity activity = getActivityFromContext(mContext);
+        Rect maxBounds = WindowBoundsHelper.getInstance().computeMaximumWindowBounds(activity);
+        return new WindowMetrics(maxBounds);
+    }
+
+    /**
      * Unwraps the hierarchy of {@link ContextWrapper}-s until {@link Activity} is reached.
      * @return Base {@link Activity} context or {@code null} if not available.
      */
diff --git a/window/window/src/main/java/androidx/window/WindowMetrics.java b/window/window/src/main/java/androidx/window/WindowMetrics.java
new file mode 100644
index 0000000..9faa5f6
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/WindowMetrics.java
@@ -0,0 +1,75 @@
+/*
+ * 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.window;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.Display;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Metrics about a {@link android.view.Window}, consisting of its bounds.
+ * <p>
+ * This is usually obtained from {@link WindowManager#getCurrentWindowMetrics()} or
+ * {@link WindowManager#getMaximumWindowMetrics()}.
+ *
+ * @see WindowManager#getCurrentWindowMetrics()
+ * @see WindowManager#getMaximumWindowMetrics()
+ */
+public final class WindowMetrics {
+    @NonNull
+    private final Rect mBounds;
+
+    /**
+     * Constructs a new {@link WindowMetrics} instance.
+     *
+     * @param bounds rect describing the bounds of the window, see {@link #getBounds}.
+     */
+    public WindowMetrics(@NonNull Rect bounds) {
+        mBounds = new Rect(bounds);
+    }
+
+    /**
+     * Returns a new {@link Rect} describing the bounds of the area the window occupies.
+     * <p>
+     * <b>Note that the size of the reported bounds can have different size than
+     * {@link Display#getSize(Point)}.</b> This method reports the window size including all system
+     * decorations, while {@link Display#getSize(Point)} reports the area excluding navigation bars
+     * and display cutout areas.
+     *
+     * @return window bounds in pixels.
+     */
+    @NonNull
+    public Rect getBounds() {
+        return new Rect(mBounds);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        WindowMetrics that = (WindowMetrics) o;
+        return mBounds.equals(that.mBounds);
+    }
+
+    @Override
+    public int hashCode() {
+        return mBounds.hashCode();
+    }
+}
diff --git a/work/workmanager-gcm/api/2.5.0-beta02.txt b/work/workmanager-gcm/api/2.5.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/workmanager-gcm/api/2.5.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/workmanager-gcm/api/public_plus_experimental_2.5.0-beta02.txt b/work/workmanager-gcm/api/public_plus_experimental_2.5.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/workmanager-gcm/api/public_plus_experimental_2.5.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/workmanager-gcm/api/res-2.5.0-beta02.txt b/work/workmanager-gcm/api/res-2.5.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/workmanager-gcm/api/res-2.5.0-beta02.txt
diff --git a/work/workmanager-gcm/api/restricted_2.5.0-beta02.txt b/work/workmanager-gcm/api/restricted_2.5.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/workmanager-gcm/api/restricted_2.5.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/workmanager-inspection/src/androidTest/AndroidManifest.xml b/work/workmanager-inspection/src/androidTest/AndroidManifest.xml
index 5275772..2c1a6a2 100644
--- a/work/workmanager-inspection/src/androidTest/AndroidManifest.xml
+++ b/work/workmanager-inspection/src/androidTest/AndroidManifest.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright 2020 The Android Open Source Project
+<?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.
@@ -13,4 +12,9 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<manifest package="androidx.work.inspection.testing"/>
\ No newline at end of file
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.work.inspection.testing">
+
+    <application android:name="androidx.work.inspection.InspectorApp" />
+
+</manifest>
\ No newline at end of file
diff --git a/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/DispatchingExecutor.kt b/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/DispatchingExecutor.kt
new file mode 100644
index 0000000..772f31f
--- /dev/null
+++ b/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/DispatchingExecutor.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.work.inspection
+
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import java.util.concurrent.Executor
+
+/**
+ * An [Executor] where we can await termination of all commands.
+ */
+class DispatchingExecutor : Executor {
+    private val job = CompletableDeferred<Unit>()
+    private val scope = CoroutineScope(Dispatchers.Default + job)
+    override fun execute(command: Runnable) {
+        scope.launch {
+            command.run()
+        }
+    }
+
+    fun runAllCommands() {
+        runBlocking {
+            job.complete(Unit)
+            job.join()
+        }
+    }
+}
\ No newline at end of file
diff --git a/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/InspectorApp.kt b/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/InspectorApp.kt
new file mode 100644
index 0000000..9de0fcc
--- /dev/null
+++ b/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/InspectorApp.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.work.inspection
+
+import android.app.Application
+import androidx.work.Configuration
+
+class InspectorApp : Application(), Configuration.Provider {
+    lateinit var executor: DispatchingExecutor
+    override fun onCreate() {
+        super.onCreate()
+        executor = DispatchingExecutor()
+    }
+
+    override fun getWorkManagerConfiguration(): Configuration {
+        return Configuration.Builder()
+            .setExecutor(executor)
+            .setTaskExecutor(executor)
+            .build()
+    }
+}
diff --git a/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/WorkInfoTest.kt b/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/WorkInfoTest.kt
index 4ebebb9..26c2af0 100644
--- a/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/WorkInfoTest.kt
+++ b/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/WorkInfoTest.kt
@@ -17,7 +17,6 @@
 package androidx.work.inspection
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import androidx.work.ExistingWorkPolicy
 import androidx.work.OneTimeWorkRequestBuilder
@@ -35,7 +34,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@FlakyTest // b/172087217
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class WorkInfoTest {
@@ -61,7 +59,7 @@
     fun addAndRemoveWork() = runBlocking {
         inspectWorkManager()
         val request = OneTimeWorkRequestBuilder<EmptyWorker>().build()
-        testEnvironment.workManager.enqueue(request)
+        testEnvironment.workManager.enqueue(request).await()
         testEnvironment.receiveEvent().let { event ->
             assertThat(event.hasWorkAdded()).isTrue()
             assertThat(event.workAdded.work.id).isEqualTo(request.stringId)
@@ -79,7 +77,7 @@
     fun sendWorkAddedEvent() = runBlocking {
         inspectWorkManager()
         val request = OneTimeWorkRequestBuilder<EmptyWorker>().build()
-        testEnvironment.workManager.enqueue(request)
+        testEnvironment.workManager.enqueue(request).await()
         testEnvironment.receiveEvent().let { event ->
             assertThat(event.hasWorkAdded()).isTrue()
             val workInfo = event.workAdded.work
@@ -92,7 +90,7 @@
     fun updateWorkInfoState() = runBlocking {
         inspectWorkManager()
         val request = OneTimeWorkRequestBuilder<EmptyWorker>().build()
-        testEnvironment.workManager.enqueue(request)
+        testEnvironment.workManager.enqueue(request).await()
         testEnvironment.receiveFilteredEvent { event ->
             event.hasWorkUpdated() && event.workUpdated.state == State.SUCCEEDED
         }.let { event ->
@@ -104,7 +102,7 @@
     fun updateWorkInfoRetryCount() = runBlocking {
         inspectWorkManager()
         val request = OneTimeWorkRequestBuilder<EmptyWorker>().build()
-        testEnvironment.workManager.enqueue(request)
+        testEnvironment.workManager.enqueue(request).await()
         testEnvironment.receiveFilteredEvent { event ->
             event.hasWorkUpdated() && event.workUpdated.runAttemptCount == 1
         }.let { event ->
@@ -116,7 +114,7 @@
     fun updateWorkInfoOutputData() = runBlocking {
         inspectWorkManager()
         val request = OneTimeWorkRequestBuilder<EmptyWorker>().build()
-        testEnvironment.workManager.enqueue(request)
+        testEnvironment.workManager.enqueue(request).await()
         testEnvironment.receiveFilteredEvent { event ->
             event.hasWorkUpdated() &&
                 event.workUpdated.hasData() &&
@@ -135,7 +133,7 @@
     fun updateWorkInfoScheduleRequestedAt() = runBlocking {
         inspectWorkManager()
         val request = OneTimeWorkRequestBuilder<EmptyWorker>().build()
-        testEnvironment.workManager.enqueue(request)
+        testEnvironment.workManager.enqueue(request).await()
         testEnvironment.receiveFilteredEvent { event ->
             event.hasWorkUpdated() &&
                 event.workUpdated.scheduleRequestedAt != WorkSpec.SCHEDULE_NOT_REQUESTED_YET
@@ -154,7 +152,7 @@
             .first()
             .asEntryHook
             .onEntry(workContinuation, listOf())
-        workContinuation.enqueue()
+        workContinuation.enqueue().await()
 
         testEnvironment.receiveEvent().let { event ->
             val workInfo = event.workAdded.work
@@ -177,6 +175,7 @@
         testEnvironment.workManager.beginUniqueWork(name, ExistingWorkPolicy.REPLACE, work1)
             .then(work2)
             .enqueue()
+            .await()
         for (count in 1..2) {
             testEnvironment.receiveEvent().let { event ->
                 assertThat(event.hasWorkAdded()).isTrue()
@@ -199,7 +198,7 @@
     fun cancelWork() = runBlocking {
         inspectWorkManager()
         val request = OneTimeWorkRequestBuilder<IdleWorker>().build()
-        testEnvironment.workManager.enqueue(request)
+        testEnvironment.workManager.enqueue(request).await()
 
         val cancelCommand = WorkManagerInspectorProtocol.CancelWorkCommand
             .newBuilder()
diff --git a/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/WorkManagerInspectorTestEnvironment.kt b/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/WorkManagerInspectorTestEnvironment.kt
index cecd31c..4a0f566 100644
--- a/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/WorkManagerInspectorTestEnvironment.kt
+++ b/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/WorkManagerInspectorTestEnvironment.kt
@@ -23,7 +23,6 @@
 import androidx.inspection.testing.TestInspectorExecutors
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.work.WorkManager
-import androidx.work.await
 import androidx.work.inspection.WorkManagerInspectorProtocol.Command
 import androidx.work.inspection.WorkManagerInspectorProtocol.Event
 import androidx.work.inspection.WorkManagerInspectorProtocol.Response
@@ -39,16 +38,18 @@
 class WorkManagerInspectorTestEnvironment : ExternalResource() {
     private lateinit var inspectorTester: InspectorTester
     private lateinit var artTooling: FakeArtTooling
+    private lateinit var application: InspectorApp
     private val job = Job()
     lateinit var workManager: WorkManager
         private set
 
     override fun before() {
         artTooling = FakeArtTooling()
-        val application = InstrumentationRegistry
+        application = InstrumentationRegistry
             .getInstrumentation()
-            .targetContext
-            .applicationContext as Application
+            .context
+            .applicationContext as InspectorApp
+
         workManager = WorkManager.getInstance(application)
 
         registerApplication(application)
@@ -65,8 +66,9 @@
 
     override fun after() {
         runBlocking {
-            workManager.cancelAllWork().await()
-            workManager.pruneWork().await()
+            workManager.cancelAllWork()
+            workManager.pruneWork()
+            application.executor.runAllCommands()
             job.cancelAndJoin()
         }
         inspectorTester.dispose()
diff --git a/work/workmanager-ktx/api/2.5.0-beta02.txt b/work/workmanager-ktx/api/2.5.0-beta02.txt
new file mode 100644
index 0000000..2c5f419
--- /dev/null
+++ b/work/workmanager-ktx/api/2.5.0-beta02.txt
@@ -0,0 +1,40 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
+    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
+    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
+    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+    method public final void onStopped();
+    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
+    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
+  }
+
+  public final class DataKt {
+    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
+    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
+  }
+
+  public final class ListenableFutureKt {
+  }
+
+  public final class OneTimeWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
+    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public final class OperationKt {
+    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS> p);
+  }
+
+  public final class PeriodicWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
+  }
+
+}
+
diff --git a/work/workmanager-ktx/api/public_plus_experimental_2.5.0-beta02.txt b/work/workmanager-ktx/api/public_plus_experimental_2.5.0-beta02.txt
new file mode 100644
index 0000000..2c5f419
--- /dev/null
+++ b/work/workmanager-ktx/api/public_plus_experimental_2.5.0-beta02.txt
@@ -0,0 +1,40 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
+    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
+    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
+    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+    method public final void onStopped();
+    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
+    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
+  }
+
+  public final class DataKt {
+    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
+    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
+  }
+
+  public final class ListenableFutureKt {
+  }
+
+  public final class OneTimeWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
+    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public final class OperationKt {
+    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS> p);
+  }
+
+  public final class PeriodicWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
+  }
+
+}
+
diff --git a/work/workmanager-ktx/api/res-2.5.0-beta02.txt b/work/workmanager-ktx/api/res-2.5.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/workmanager-ktx/api/res-2.5.0-beta02.txt
diff --git a/work/workmanager-ktx/api/restricted_2.5.0-beta02.txt b/work/workmanager-ktx/api/restricted_2.5.0-beta02.txt
new file mode 100644
index 0000000..2c5f419
--- /dev/null
+++ b/work/workmanager-ktx/api/restricted_2.5.0-beta02.txt
@@ -0,0 +1,40 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
+    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
+    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
+    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+    method public final void onStopped();
+    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
+    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
+  }
+
+  public final class DataKt {
+    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
+    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
+  }
+
+  public final class ListenableFutureKt {
+  }
+
+  public final class OneTimeWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
+    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public final class OperationKt {
+    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS> p);
+  }
+
+  public final class PeriodicWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
+  }
+
+}
+
diff --git a/work/workmanager-multiprocess/api/2.5.0-beta02.txt b/work/workmanager-multiprocess/api/2.5.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/workmanager-multiprocess/api/2.5.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/workmanager-multiprocess/api/public_plus_experimental_2.5.0-beta02.txt b/work/workmanager-multiprocess/api/public_plus_experimental_2.5.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/workmanager-multiprocess/api/public_plus_experimental_2.5.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/workmanager-multiprocess/api/res-2.5.0-beta02.txt b/work/workmanager-multiprocess/api/res-2.5.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/workmanager-multiprocess/api/res-2.5.0-beta02.txt
diff --git a/work/workmanager-multiprocess/api/restricted_2.5.0-beta02.txt b/work/workmanager-multiprocess/api/restricted_2.5.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/workmanager-multiprocess/api/restricted_2.5.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/workmanager-rxjava2/api/2.5.0-beta02.txt b/work/workmanager-rxjava2/api/2.5.0-beta02.txt
new file mode 100644
index 0000000..2d667ee
--- /dev/null
+++ b/work/workmanager-rxjava2/api/2.5.0-beta02.txt
@@ -0,0 +1,14 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.Scheduler getBackgroundScheduler();
+    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
+    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/workmanager-rxjava2/api/public_plus_experimental_2.5.0-beta02.txt b/work/workmanager-rxjava2/api/public_plus_experimental_2.5.0-beta02.txt
new file mode 100644
index 0000000..2d667ee
--- /dev/null
+++ b/work/workmanager-rxjava2/api/public_plus_experimental_2.5.0-beta02.txt
@@ -0,0 +1,14 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.Scheduler getBackgroundScheduler();
+    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
+    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/workmanager-rxjava2/api/res-2.5.0-beta02.txt b/work/workmanager-rxjava2/api/res-2.5.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/workmanager-rxjava2/api/res-2.5.0-beta02.txt
diff --git a/work/workmanager-rxjava2/api/restricted_2.5.0-beta02.txt b/work/workmanager-rxjava2/api/restricted_2.5.0-beta02.txt
new file mode 100644
index 0000000..2d667ee
--- /dev/null
+++ b/work/workmanager-rxjava2/api/restricted_2.5.0-beta02.txt
@@ -0,0 +1,14 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.Scheduler getBackgroundScheduler();
+    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
+    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/workmanager-rxjava3/api/2.5.0-beta02.txt b/work/workmanager-rxjava3/api/2.5.0-beta02.txt
new file mode 100644
index 0000000..9293340
--- /dev/null
+++ b/work/workmanager-rxjava3/api/2.5.0-beta02.txt
@@ -0,0 +1,13 @@
+// Signature format: 4.0
+package androidx.work.rxjava3 {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
+    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/workmanager-rxjava3/api/public_plus_experimental_2.5.0-beta02.txt b/work/workmanager-rxjava3/api/public_plus_experimental_2.5.0-beta02.txt
new file mode 100644
index 0000000..9293340
--- /dev/null
+++ b/work/workmanager-rxjava3/api/public_plus_experimental_2.5.0-beta02.txt
@@ -0,0 +1,13 @@
+// Signature format: 4.0
+package androidx.work.rxjava3 {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
+    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/workmanager-rxjava3/api/res-2.5.0-beta02.txt b/work/workmanager-rxjava3/api/res-2.5.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/workmanager-rxjava3/api/res-2.5.0-beta02.txt
diff --git a/work/workmanager-rxjava3/api/restricted_2.5.0-beta02.txt b/work/workmanager-rxjava3/api/restricted_2.5.0-beta02.txt
new file mode 100644
index 0000000..9293340
--- /dev/null
+++ b/work/workmanager-rxjava3/api/restricted_2.5.0-beta02.txt
@@ -0,0 +1,13 @@
+// Signature format: 4.0
+package androidx.work.rxjava3 {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
+    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/workmanager-testing/api/2.5.0-beta02.txt b/work/workmanager-testing/api/2.5.0-beta02.txt
new file mode 100644
index 0000000..f3f3fe2
--- /dev/null
+++ b/work/workmanager-testing/api/2.5.0-beta02.txt
@@ -0,0 +1,52 @@
+// Signature format: 4.0
+package androidx.work.testing {
+
+  public class SynchronousExecutor implements java.util.concurrent.Executor {
+    ctor public SynchronousExecutor();
+    method public void execute(Runnable);
+  }
+
+  public interface TestDriver {
+    method public void setAllConstraintsMet(java.util.UUID);
+    method public void setInitialDelayMet(java.util.UUID);
+    method public void setPeriodDelayMet(java.util.UUID);
+  }
+
+  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
+    method public W build();
+    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
+    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
+    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public final class TestListenableWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
+    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
+    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
+  }
+
+  public final class TestWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public final class WorkManagerTestInitHelper {
+    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
+    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
+  }
+
+}
+
diff --git a/work/workmanager-testing/api/public_plus_experimental_2.5.0-beta02.txt b/work/workmanager-testing/api/public_plus_experimental_2.5.0-beta02.txt
new file mode 100644
index 0000000..f3f3fe2
--- /dev/null
+++ b/work/workmanager-testing/api/public_plus_experimental_2.5.0-beta02.txt
@@ -0,0 +1,52 @@
+// Signature format: 4.0
+package androidx.work.testing {
+
+  public class SynchronousExecutor implements java.util.concurrent.Executor {
+    ctor public SynchronousExecutor();
+    method public void execute(Runnable);
+  }
+
+  public interface TestDriver {
+    method public void setAllConstraintsMet(java.util.UUID);
+    method public void setInitialDelayMet(java.util.UUID);
+    method public void setPeriodDelayMet(java.util.UUID);
+  }
+
+  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
+    method public W build();
+    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
+    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
+    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public final class TestListenableWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
+    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
+    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
+  }
+
+  public final class TestWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public final class WorkManagerTestInitHelper {
+    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
+    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
+  }
+
+}
+
diff --git a/work/workmanager-testing/api/res-2.5.0-beta02.txt b/work/workmanager-testing/api/res-2.5.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/workmanager-testing/api/res-2.5.0-beta02.txt
diff --git a/work/workmanager-testing/api/restricted_2.5.0-beta02.txt b/work/workmanager-testing/api/restricted_2.5.0-beta02.txt
new file mode 100644
index 0000000..f3f3fe2
--- /dev/null
+++ b/work/workmanager-testing/api/restricted_2.5.0-beta02.txt
@@ -0,0 +1,52 @@
+// Signature format: 4.0
+package androidx.work.testing {
+
+  public class SynchronousExecutor implements java.util.concurrent.Executor {
+    ctor public SynchronousExecutor();
+    method public void execute(Runnable);
+  }
+
+  public interface TestDriver {
+    method public void setAllConstraintsMet(java.util.UUID);
+    method public void setInitialDelayMet(java.util.UUID);
+    method public void setPeriodDelayMet(java.util.UUID);
+  }
+
+  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
+    method public W build();
+    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
+    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
+    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public final class TestListenableWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
+    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
+    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
+  }
+
+  public final class TestWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public final class WorkManagerTestInitHelper {
+    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
+    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
+  }
+
+}
+
diff --git a/work/workmanager/api/2.5.0-beta02.txt b/work/workmanager/api/2.5.0-beta02.txt
new file mode 100644
index 0000000..29b0d40
--- /dev/null
+++ b/work/workmanager/api/2.5.0-beta02.txt
@@ -0,0 +1,392 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
+    ctor public ArrayCreatingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public enum BackoffPolicy {
+    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
+    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
+  }
+
+  public final class Configuration {
+    method public String? getDefaultProcessName();
+    method public java.util.concurrent.Executor getExecutor();
+    method public androidx.work.InputMergerFactory getInputMergerFactory();
+    method public int getMaxJobSchedulerId();
+    method public int getMinJobSchedulerId();
+    method public androidx.work.RunnableScheduler getRunnableScheduler();
+    method public java.util.concurrent.Executor getTaskExecutor();
+    method public androidx.work.WorkerFactory getWorkerFactory();
+    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
+  }
+
+  public static final class Configuration.Builder {
+    ctor public Configuration.Builder();
+    method public androidx.work.Configuration build();
+    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
+    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
+    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
+    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
+    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
+    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
+    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public static interface Configuration.Provider {
+    method public androidx.work.Configuration getWorkManagerConfiguration();
+  }
+
+  public final class Constraints {
+    ctor public Constraints(androidx.work.Constraints);
+    method public androidx.work.NetworkType getRequiredNetworkType();
+    method public boolean requiresBatteryNotLow();
+    method public boolean requiresCharging();
+    method @RequiresApi(23) public boolean requiresDeviceIdle();
+    method public boolean requiresStorageNotLow();
+    field public static final androidx.work.Constraints NONE;
+  }
+
+  public static final class Constraints.Builder {
+    ctor public Constraints.Builder();
+    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri, boolean);
+    method public androidx.work.Constraints build();
+    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType);
+    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean);
+    method public androidx.work.Constraints.Builder setRequiresCharging(boolean);
+    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean);
+    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration!);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration!);
+  }
+
+  public final class Data {
+    ctor public Data(androidx.work.Data);
+    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
+    method public boolean getBoolean(String, boolean);
+    method public boolean[]? getBooleanArray(String);
+    method public byte getByte(String, byte);
+    method public byte[]? getByteArray(String);
+    method public double getDouble(String, double);
+    method public double[]? getDoubleArray(String);
+    method public float getFloat(String, float);
+    method public float[]? getFloatArray(String);
+    method public int getInt(String, int);
+    method public int[]? getIntArray(String);
+    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
+    method public long getLong(String, long);
+    method public long[]? getLongArray(String);
+    method public String? getString(String);
+    method public String![]? getStringArray(String);
+    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
+    method public byte[] toByteArray();
+    field public static final androidx.work.Data EMPTY;
+    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
+  }
+
+  public static final class Data.Builder {
+    ctor public Data.Builder();
+    method public androidx.work.Data build();
+    method public androidx.work.Data.Builder putAll(androidx.work.Data);
+    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
+    method public androidx.work.Data.Builder putBoolean(String, boolean);
+    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
+    method public androidx.work.Data.Builder putByte(String, byte);
+    method public androidx.work.Data.Builder putByteArray(String, byte[]);
+    method public androidx.work.Data.Builder putDouble(String, double);
+    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
+    method public androidx.work.Data.Builder putFloat(String, float);
+    method public androidx.work.Data.Builder putFloatArray(String, float[]);
+    method public androidx.work.Data.Builder putInt(String, int);
+    method public androidx.work.Data.Builder putIntArray(String, int[]);
+    method public androidx.work.Data.Builder putLong(String, long);
+    method public androidx.work.Data.Builder putLongArray(String, long[]);
+    method public androidx.work.Data.Builder putString(String, String?);
+    method public androidx.work.Data.Builder putStringArray(String, String![]);
+  }
+
+  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
+    ctor public DelegatingWorkerFactory();
+    method public final void addFactory(androidx.work.WorkerFactory);
+    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public enum ExistingPeriodicWorkPolicy {
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
+  }
+
+  public enum ExistingWorkPolicy {
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
+    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
+    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
+  }
+
+  public final class ForegroundInfo {
+    ctor public ForegroundInfo(int, android.app.Notification);
+    ctor public ForegroundInfo(int, android.app.Notification, int);
+    method public int getForegroundServiceType();
+    method public android.app.Notification getNotification();
+    method public int getNotificationId();
+  }
+
+  public interface ForegroundUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
+  }
+
+  public abstract class InputMerger {
+    ctor public InputMerger();
+    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public abstract class InputMergerFactory {
+    ctor public InputMergerFactory();
+    method public abstract androidx.work.InputMerger? createInputMerger(String);
+  }
+
+  public abstract class ListenableWorker {
+    ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public final android.content.Context getApplicationContext();
+    method public final java.util.UUID getId();
+    method public final androidx.work.Data getInputData();
+    method @RequiresApi(28) public final android.net.Network? getNetwork();
+    method @IntRange(from=0) public final int getRunAttemptCount();
+    method public final java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
+    method public final boolean isStopped();
+    method public void onStopped();
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
+    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract static class ListenableWorker.Result {
+    method public static androidx.work.ListenableWorker.Result failure();
+    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
+    method public static androidx.work.ListenableWorker.Result retry();
+    method public static androidx.work.ListenableWorker.Result success();
+    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
+  }
+
+  public enum NetworkType {
+    enum_constant public static final androidx.work.NetworkType CONNECTED;
+    enum_constant public static final androidx.work.NetworkType METERED;
+    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
+    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+    enum_constant public static final androidx.work.NetworkType UNMETERED;
+  }
+
+  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
+    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker>);
+    method public static java.util.List<androidx.work.OneTimeWorkRequest!> from(java.util.List<java.lang.Class<? extends androidx.work.ListenableWorker>!>);
+  }
+
+  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
+    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>);
+    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger>);
+  }
+
+  public interface Operation {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
+    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
+  }
+
+  public abstract static class Operation.State {
+  }
+
+  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
+    ctor public Operation.State.FAILURE(Throwable);
+    method public Throwable getThrowable();
+  }
+
+  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
+  }
+
+  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
+  }
+
+  public final class OverwritingInputMerger extends androidx.work.InputMerger {
+    ctor public OverwritingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
+    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
+    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
+  }
+
+  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration);
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration, java.time.Duration);
+  }
+
+  public interface ProgressUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
+  }
+
+  public interface RunnableScheduler {
+    method public void cancel(Runnable);
+    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
+  }
+
+  public abstract class WorkContinuation {
+    ctor public WorkContinuation();
+    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
+    method public abstract androidx.work.Operation enqueue();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
+    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public final class WorkInfo {
+    method public java.util.UUID getId();
+    method public androidx.work.Data getOutputData();
+    method public androidx.work.Data getProgress();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public androidx.work.WorkInfo.State getState();
+    method public java.util.Set<java.lang.String!> getTags();
+  }
+
+  public enum WorkInfo.State {
+    method public boolean isFinished();
+    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
+    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
+    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
+    enum_constant public static final androidx.work.WorkInfo.State FAILED;
+    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
+    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
+  }
+
+  public abstract class WorkManager {
+    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Operation cancelAllWork();
+    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
+    method public abstract androidx.work.Operation cancelUniqueWork(String);
+    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
+    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
+    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
+    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
+    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method @Deprecated public static androidx.work.WorkManager getInstance();
+    method public static androidx.work.WorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
+    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
+    method public static void initialize(android.content.Context, androidx.work.Configuration);
+    method public abstract androidx.work.Operation pruneWork();
+  }
+
+  public final class WorkQuery {
+    method public java.util.List<java.util.UUID!> getIds();
+    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
+    method public java.util.List<java.lang.String!> getTags();
+    method public java.util.List<java.lang.String!> getUniqueWorkNames();
+  }
+
+  public static final class WorkQuery.Builder {
+    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
+    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery build();
+    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
+  }
+
+  public abstract class WorkRequest {
+    method public java.util.UUID getId();
+    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
+    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
+    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
+  }
+
+  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<?, ?>, W extends androidx.work.WorkRequest> {
+    method public final B addTag(String);
+    method public final W build();
+    method public final B keepResultsForAtLeast(long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration);
+    method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
+    method public final B setConstraints(androidx.work.Constraints);
+    method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
+    method public final B setInputData(androidx.work.Data);
+  }
+
+  public abstract class Worker extends androidx.work.ListenableWorker {
+    ctor @Keep public Worker(android.content.Context, androidx.work.WorkerParameters);
+    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract class WorkerFactory {
+    ctor public WorkerFactory();
+    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public final class WorkerParameters {
+    method public java.util.UUID getId();
+    method public androidx.work.Data getInputData();
+    method @RequiresApi(28) public android.net.Network? getNetwork();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
+  }
+
+}
+
+package androidx.work.multiprocess {
+
+  public abstract class RemoteWorkContinuation {
+    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
+    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public abstract class RemoteWorkManager {
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+  }
+
+}
+
diff --git a/work/workmanager/api/public_plus_experimental_2.5.0-beta02.txt b/work/workmanager/api/public_plus_experimental_2.5.0-beta02.txt
new file mode 100644
index 0000000..29b0d40
--- /dev/null
+++ b/work/workmanager/api/public_plus_experimental_2.5.0-beta02.txt
@@ -0,0 +1,392 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
+    ctor public ArrayCreatingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public enum BackoffPolicy {
+    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
+    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
+  }
+
+  public final class Configuration {
+    method public String? getDefaultProcessName();
+    method public java.util.concurrent.Executor getExecutor();
+    method public androidx.work.InputMergerFactory getInputMergerFactory();
+    method public int getMaxJobSchedulerId();
+    method public int getMinJobSchedulerId();
+    method public androidx.work.RunnableScheduler getRunnableScheduler();
+    method public java.util.concurrent.Executor getTaskExecutor();
+    method public androidx.work.WorkerFactory getWorkerFactory();
+    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
+  }
+
+  public static final class Configuration.Builder {
+    ctor public Configuration.Builder();
+    method public androidx.work.Configuration build();
+    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
+    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
+    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
+    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
+    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
+    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
+    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public static interface Configuration.Provider {
+    method public androidx.work.Configuration getWorkManagerConfiguration();
+  }
+
+  public final class Constraints {
+    ctor public Constraints(androidx.work.Constraints);
+    method public androidx.work.NetworkType getRequiredNetworkType();
+    method public boolean requiresBatteryNotLow();
+    method public boolean requiresCharging();
+    method @RequiresApi(23) public boolean requiresDeviceIdle();
+    method public boolean requiresStorageNotLow();
+    field public static final androidx.work.Constraints NONE;
+  }
+
+  public static final class Constraints.Builder {
+    ctor public Constraints.Builder();
+    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri, boolean);
+    method public androidx.work.Constraints build();
+    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType);
+    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean);
+    method public androidx.work.Constraints.Builder setRequiresCharging(boolean);
+    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean);
+    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration!);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration!);
+  }
+
+  public final class Data {
+    ctor public Data(androidx.work.Data);
+    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
+    method public boolean getBoolean(String, boolean);
+    method public boolean[]? getBooleanArray(String);
+    method public byte getByte(String, byte);
+    method public byte[]? getByteArray(String);
+    method public double getDouble(String, double);
+    method public double[]? getDoubleArray(String);
+    method public float getFloat(String, float);
+    method public float[]? getFloatArray(String);
+    method public int getInt(String, int);
+    method public int[]? getIntArray(String);
+    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
+    method public long getLong(String, long);
+    method public long[]? getLongArray(String);
+    method public String? getString(String);
+    method public String![]? getStringArray(String);
+    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
+    method public byte[] toByteArray();
+    field public static final androidx.work.Data EMPTY;
+    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
+  }
+
+  public static final class Data.Builder {
+    ctor public Data.Builder();
+    method public androidx.work.Data build();
+    method public androidx.work.Data.Builder putAll(androidx.work.Data);
+    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
+    method public androidx.work.Data.Builder putBoolean(String, boolean);
+    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
+    method public androidx.work.Data.Builder putByte(String, byte);
+    method public androidx.work.Data.Builder putByteArray(String, byte[]);
+    method public androidx.work.Data.Builder putDouble(String, double);
+    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
+    method public androidx.work.Data.Builder putFloat(String, float);
+    method public androidx.work.Data.Builder putFloatArray(String, float[]);
+    method public androidx.work.Data.Builder putInt(String, int);
+    method public androidx.work.Data.Builder putIntArray(String, int[]);
+    method public androidx.work.Data.Builder putLong(String, long);
+    method public androidx.work.Data.Builder putLongArray(String, long[]);
+    method public androidx.work.Data.Builder putString(String, String?);
+    method public androidx.work.Data.Builder putStringArray(String, String![]);
+  }
+
+  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
+    ctor public DelegatingWorkerFactory();
+    method public final void addFactory(androidx.work.WorkerFactory);
+    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public enum ExistingPeriodicWorkPolicy {
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
+  }
+
+  public enum ExistingWorkPolicy {
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
+    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
+    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
+  }
+
+  public final class ForegroundInfo {
+    ctor public ForegroundInfo(int, android.app.Notification);
+    ctor public ForegroundInfo(int, android.app.Notification, int);
+    method public int getForegroundServiceType();
+    method public android.app.Notification getNotification();
+    method public int getNotificationId();
+  }
+
+  public interface ForegroundUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
+  }
+
+  public abstract class InputMerger {
+    ctor public InputMerger();
+    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public abstract class InputMergerFactory {
+    ctor public InputMergerFactory();
+    method public abstract androidx.work.InputMerger? createInputMerger(String);
+  }
+
+  public abstract class ListenableWorker {
+    ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public final android.content.Context getApplicationContext();
+    method public final java.util.UUID getId();
+    method public final androidx.work.Data getInputData();
+    method @RequiresApi(28) public final android.net.Network? getNetwork();
+    method @IntRange(from=0) public final int getRunAttemptCount();
+    method public final java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
+    method public final boolean isStopped();
+    method public void onStopped();
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
+    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract static class ListenableWorker.Result {
+    method public static androidx.work.ListenableWorker.Result failure();
+    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
+    method public static androidx.work.ListenableWorker.Result retry();
+    method public static androidx.work.ListenableWorker.Result success();
+    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
+  }
+
+  public enum NetworkType {
+    enum_constant public static final androidx.work.NetworkType CONNECTED;
+    enum_constant public static final androidx.work.NetworkType METERED;
+    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
+    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+    enum_constant public static final androidx.work.NetworkType UNMETERED;
+  }
+
+  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
+    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker>);
+    method public static java.util.List<androidx.work.OneTimeWorkRequest!> from(java.util.List<java.lang.Class<? extends androidx.work.ListenableWorker>!>);
+  }
+
+  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
+    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>);
+    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger>);
+  }
+
+  public interface Operation {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
+    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
+  }
+
+  public abstract static class Operation.State {
+  }
+
+  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
+    ctor public Operation.State.FAILURE(Throwable);
+    method public Throwable getThrowable();
+  }
+
+  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
+  }
+
+  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
+  }
+
+  public final class OverwritingInputMerger extends androidx.work.InputMerger {
+    ctor public OverwritingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
+    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
+    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
+  }
+
+  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration);
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration, java.time.Duration);
+  }
+
+  public interface ProgressUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
+  }
+
+  public interface RunnableScheduler {
+    method public void cancel(Runnable);
+    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
+  }
+
+  public abstract class WorkContinuation {
+    ctor public WorkContinuation();
+    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
+    method public abstract androidx.work.Operation enqueue();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
+    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public final class WorkInfo {
+    method public java.util.UUID getId();
+    method public androidx.work.Data getOutputData();
+    method public androidx.work.Data getProgress();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public androidx.work.WorkInfo.State getState();
+    method public java.util.Set<java.lang.String!> getTags();
+  }
+
+  public enum WorkInfo.State {
+    method public boolean isFinished();
+    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
+    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
+    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
+    enum_constant public static final androidx.work.WorkInfo.State FAILED;
+    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
+    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
+  }
+
+  public abstract class WorkManager {
+    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Operation cancelAllWork();
+    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
+    method public abstract androidx.work.Operation cancelUniqueWork(String);
+    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
+    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
+    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
+    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
+    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method @Deprecated public static androidx.work.WorkManager getInstance();
+    method public static androidx.work.WorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
+    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
+    method public static void initialize(android.content.Context, androidx.work.Configuration);
+    method public abstract androidx.work.Operation pruneWork();
+  }
+
+  public final class WorkQuery {
+    method public java.util.List<java.util.UUID!> getIds();
+    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
+    method public java.util.List<java.lang.String!> getTags();
+    method public java.util.List<java.lang.String!> getUniqueWorkNames();
+  }
+
+  public static final class WorkQuery.Builder {
+    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
+    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery build();
+    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
+  }
+
+  public abstract class WorkRequest {
+    method public java.util.UUID getId();
+    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
+    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
+    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
+  }
+
+  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<?, ?>, W extends androidx.work.WorkRequest> {
+    method public final B addTag(String);
+    method public final W build();
+    method public final B keepResultsForAtLeast(long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration);
+    method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
+    method public final B setConstraints(androidx.work.Constraints);
+    method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
+    method public final B setInputData(androidx.work.Data);
+  }
+
+  public abstract class Worker extends androidx.work.ListenableWorker {
+    ctor @Keep public Worker(android.content.Context, androidx.work.WorkerParameters);
+    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract class WorkerFactory {
+    ctor public WorkerFactory();
+    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public final class WorkerParameters {
+    method public java.util.UUID getId();
+    method public androidx.work.Data getInputData();
+    method @RequiresApi(28) public android.net.Network? getNetwork();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
+  }
+
+}
+
+package androidx.work.multiprocess {
+
+  public abstract class RemoteWorkContinuation {
+    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
+    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public abstract class RemoteWorkManager {
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+  }
+
+}
+
diff --git a/work/workmanager/api/res-2.5.0-beta02.txt b/work/workmanager/api/res-2.5.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/workmanager/api/res-2.5.0-beta02.txt
diff --git a/work/workmanager/api/restricted_2.5.0-beta02.txt b/work/workmanager/api/restricted_2.5.0-beta02.txt
new file mode 100644
index 0000000..29b0d40
--- /dev/null
+++ b/work/workmanager/api/restricted_2.5.0-beta02.txt
@@ -0,0 +1,392 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
+    ctor public ArrayCreatingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public enum BackoffPolicy {
+    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
+    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
+  }
+
+  public final class Configuration {
+    method public String? getDefaultProcessName();
+    method public java.util.concurrent.Executor getExecutor();
+    method public androidx.work.InputMergerFactory getInputMergerFactory();
+    method public int getMaxJobSchedulerId();
+    method public int getMinJobSchedulerId();
+    method public androidx.work.RunnableScheduler getRunnableScheduler();
+    method public java.util.concurrent.Executor getTaskExecutor();
+    method public androidx.work.WorkerFactory getWorkerFactory();
+    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
+  }
+
+  public static final class Configuration.Builder {
+    ctor public Configuration.Builder();
+    method public androidx.work.Configuration build();
+    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
+    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
+    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
+    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
+    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
+    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
+    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public static interface Configuration.Provider {
+    method public androidx.work.Configuration getWorkManagerConfiguration();
+  }
+
+  public final class Constraints {
+    ctor public Constraints(androidx.work.Constraints);
+    method public androidx.work.NetworkType getRequiredNetworkType();
+    method public boolean requiresBatteryNotLow();
+    method public boolean requiresCharging();
+    method @RequiresApi(23) public boolean requiresDeviceIdle();
+    method public boolean requiresStorageNotLow();
+    field public static final androidx.work.Constraints NONE;
+  }
+
+  public static final class Constraints.Builder {
+    ctor public Constraints.Builder();
+    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri, boolean);
+    method public androidx.work.Constraints build();
+    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType);
+    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean);
+    method public androidx.work.Constraints.Builder setRequiresCharging(boolean);
+    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean);
+    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration!);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration!);
+  }
+
+  public final class Data {
+    ctor public Data(androidx.work.Data);
+    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
+    method public boolean getBoolean(String, boolean);
+    method public boolean[]? getBooleanArray(String);
+    method public byte getByte(String, byte);
+    method public byte[]? getByteArray(String);
+    method public double getDouble(String, double);
+    method public double[]? getDoubleArray(String);
+    method public float getFloat(String, float);
+    method public float[]? getFloatArray(String);
+    method public int getInt(String, int);
+    method public int[]? getIntArray(String);
+    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
+    method public long getLong(String, long);
+    method public long[]? getLongArray(String);
+    method public String? getString(String);
+    method public String![]? getStringArray(String);
+    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
+    method public byte[] toByteArray();
+    field public static final androidx.work.Data EMPTY;
+    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
+  }
+
+  public static final class Data.Builder {
+    ctor public Data.Builder();
+    method public androidx.work.Data build();
+    method public androidx.work.Data.Builder putAll(androidx.work.Data);
+    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
+    method public androidx.work.Data.Builder putBoolean(String, boolean);
+    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
+    method public androidx.work.Data.Builder putByte(String, byte);
+    method public androidx.work.Data.Builder putByteArray(String, byte[]);
+    method public androidx.work.Data.Builder putDouble(String, double);
+    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
+    method public androidx.work.Data.Builder putFloat(String, float);
+    method public androidx.work.Data.Builder putFloatArray(String, float[]);
+    method public androidx.work.Data.Builder putInt(String, int);
+    method public androidx.work.Data.Builder putIntArray(String, int[]);
+    method public androidx.work.Data.Builder putLong(String, long);
+    method public androidx.work.Data.Builder putLongArray(String, long[]);
+    method public androidx.work.Data.Builder putString(String, String?);
+    method public androidx.work.Data.Builder putStringArray(String, String![]);
+  }
+
+  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
+    ctor public DelegatingWorkerFactory();
+    method public final void addFactory(androidx.work.WorkerFactory);
+    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public enum ExistingPeriodicWorkPolicy {
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
+  }
+
+  public enum ExistingWorkPolicy {
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
+    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
+    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
+  }
+
+  public final class ForegroundInfo {
+    ctor public ForegroundInfo(int, android.app.Notification);
+    ctor public ForegroundInfo(int, android.app.Notification, int);
+    method public int getForegroundServiceType();
+    method public android.app.Notification getNotification();
+    method public int getNotificationId();
+  }
+
+  public interface ForegroundUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
+  }
+
+  public abstract class InputMerger {
+    ctor public InputMerger();
+    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public abstract class InputMergerFactory {
+    ctor public InputMergerFactory();
+    method public abstract androidx.work.InputMerger? createInputMerger(String);
+  }
+
+  public abstract class ListenableWorker {
+    ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public final android.content.Context getApplicationContext();
+    method public final java.util.UUID getId();
+    method public final androidx.work.Data getInputData();
+    method @RequiresApi(28) public final android.net.Network? getNetwork();
+    method @IntRange(from=0) public final int getRunAttemptCount();
+    method public final java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
+    method public final boolean isStopped();
+    method public void onStopped();
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
+    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract static class ListenableWorker.Result {
+    method public static androidx.work.ListenableWorker.Result failure();
+    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
+    method public static androidx.work.ListenableWorker.Result retry();
+    method public static androidx.work.ListenableWorker.Result success();
+    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
+  }
+
+  public enum NetworkType {
+    enum_constant public static final androidx.work.NetworkType CONNECTED;
+    enum_constant public static final androidx.work.NetworkType METERED;
+    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
+    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+    enum_constant public static final androidx.work.NetworkType UNMETERED;
+  }
+
+  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
+    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker>);
+    method public static java.util.List<androidx.work.OneTimeWorkRequest!> from(java.util.List<java.lang.Class<? extends androidx.work.ListenableWorker>!>);
+  }
+
+  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
+    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>);
+    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger>);
+  }
+
+  public interface Operation {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
+    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
+  }
+
+  public abstract static class Operation.State {
+  }
+
+  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
+    ctor public Operation.State.FAILURE(Throwable);
+    method public Throwable getThrowable();
+  }
+
+  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
+  }
+
+  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
+  }
+
+  public final class OverwritingInputMerger extends androidx.work.InputMerger {
+    ctor public OverwritingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
+    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
+    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
+  }
+
+  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration);
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration, java.time.Duration);
+  }
+
+  public interface ProgressUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
+  }
+
+  public interface RunnableScheduler {
+    method public void cancel(Runnable);
+    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
+  }
+
+  public abstract class WorkContinuation {
+    ctor public WorkContinuation();
+    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
+    method public abstract androidx.work.Operation enqueue();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
+    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public final class WorkInfo {
+    method public java.util.UUID getId();
+    method public androidx.work.Data getOutputData();
+    method public androidx.work.Data getProgress();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public androidx.work.WorkInfo.State getState();
+    method public java.util.Set<java.lang.String!> getTags();
+  }
+
+  public enum WorkInfo.State {
+    method public boolean isFinished();
+    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
+    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
+    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
+    enum_constant public static final androidx.work.WorkInfo.State FAILED;
+    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
+    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
+  }
+
+  public abstract class WorkManager {
+    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Operation cancelAllWork();
+    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
+    method public abstract androidx.work.Operation cancelUniqueWork(String);
+    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
+    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
+    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
+    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
+    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method @Deprecated public static androidx.work.WorkManager getInstance();
+    method public static androidx.work.WorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
+    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
+    method public static void initialize(android.content.Context, androidx.work.Configuration);
+    method public abstract androidx.work.Operation pruneWork();
+  }
+
+  public final class WorkQuery {
+    method public java.util.List<java.util.UUID!> getIds();
+    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
+    method public java.util.List<java.lang.String!> getTags();
+    method public java.util.List<java.lang.String!> getUniqueWorkNames();
+  }
+
+  public static final class WorkQuery.Builder {
+    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
+    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery build();
+    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
+  }
+
+  public abstract class WorkRequest {
+    method public java.util.UUID getId();
+    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
+    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
+    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
+  }
+
+  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<?, ?>, W extends androidx.work.WorkRequest> {
+    method public final B addTag(String);
+    method public final W build();
+    method public final B keepResultsForAtLeast(long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration);
+    method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
+    method public final B setConstraints(androidx.work.Constraints);
+    method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
+    method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
+    method public final B setInputData(androidx.work.Data);
+  }
+
+  public abstract class Worker extends androidx.work.ListenableWorker {
+    ctor @Keep public Worker(android.content.Context, androidx.work.WorkerParameters);
+    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract class WorkerFactory {
+    ctor public WorkerFactory();
+    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public final class WorkerParameters {
+    method public java.util.UUID getId();
+    method public androidx.work.Data getInputData();
+    method @RequiresApi(28) public android.net.Network? getNetwork();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
+  }
+
+}
+
+package androidx.work.multiprocess {
+
+  public abstract class RemoteWorkContinuation {
+    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
+    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public abstract class RemoteWorkManager {
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+  }
+
+}
+
diff --git a/work/workmanager/src/main/java/androidx/work/impl/Processor.java b/work/workmanager/src/main/java/androidx/work/impl/Processor.java
index 129294d..17e7584 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/Processor.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/Processor.java
@@ -309,7 +309,14 @@
             boolean hasForegroundWork = !mForegroundWorkMap.isEmpty();
             if (!hasForegroundWork) {
                 Intent intent = createStopForegroundIntent(mAppContext);
-                mAppContext.startService(intent);
+                try {
+                    // Wrapping this inside a try..catch, because there are bugs the platform
+                    // that cause an IllegalStateException when an intent is dispatched to stop
+                    // the foreground service that is running.
+                    mAppContext.startService(intent);
+                } catch (Throwable throwable) {
+                    Logger.get().error(TAG, "Unable to stop foreground service", throwable);
+                }
                 // Release wake lock if there is no more pending work.
                 if (mForegroundLock != null) {
                     mForegroundLock.release();
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
index 2428b9e..6e39078 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
@@ -321,6 +321,7 @@
                     // observing scheduling limits this bit needs to be reset.
                     workSpecDao.markWorkSpecScheduled(workSpecId, SCHEDULE_NOT_REQUESTED_YET);
                 }
+                workDatabase.setTransactionSuccessful();
             } finally {
                 workDatabase.endTransaction();
             }