[go: nahoru, domu]

Merge "Add MacrobenchmarkRule as primary API surface" into androidx-master-dev
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/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/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 2bb6f18..31a5aa0 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")
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 8839e7a..bfa3df9 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,885 @@
 
 }
 
+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.serialization {
 
   public final class Bundleable implements android.os.Parcelable {
@@ -171,6 +1051,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..bfa3df9 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,885 @@
 
 }
 
+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.serialization {
 
   public final class Bundleable implements android.os.Parcelable {
@@ -171,6 +1051,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..bfa3df9 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,885 @@
 
 }
 
+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.serialization {
 
   public final class Bundleable implements android.os.Parcelable {
@@ -171,6 +1051,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..1d48c37
--- /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.time.Duration;
+
+/** 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),
+                    Duration.ofHours(1).getSeconds(),
+                    createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific"));
+    private final TravelEstimate mDestinationTravelEstimate =
+            TravelEstimate.create(
+                    Distance.create(/* displayDistance= */ 100, Distance.UNIT_KILOMETERS),
+                    Duration.ofHours(1).getSeconds(),
+                    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..2528dfb
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
@@ -0,0 +1,449 @@
+/*
+ * 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 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.time.Duration;
+import java.time.ZonedDateTime;
+
+/** 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),
+                        Duration.ofHours(1),
+                        ZonedDateTime.parse("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),
+                        Duration.ofHours(1),
+                        ZonedDateTime.parse("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),
+                        Duration.ofHours(1),
+                        ZonedDateTime.parse("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),
+                        Duration.ofHours(1),
+                        ZonedDateTime.parse("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),
+                                                Duration.ofHours(1),
+                                                ZonedDateTime.parse(
+                                                        "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..2e95d5e
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TravelEstimateTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.assertDateTimeWithZoneEquals;
+import static androidx.car.app.TestUtils.createDateTimeWithZone;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Build.VERSION_CODES;
+
+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 org.robolectric.annotation.Config;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+/** 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 Duration mRemainingTime = Duration.ofHours(10);
+
+    @Test
+    @Config(minSdk = VERSION_CODES.O)
+    public void create_duration() {
+        ZonedDateTime arrivalTime = ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]");
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(mRemainingDistance, mRemainingTime, arrivalTime);
+
+        assertThat(travelEstimate.getRemainingDistance()).isEqualTo(mRemainingDistance);
+        assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(mRemainingTime.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);
+        Duration remainingTime = Duration.ofHours(10);
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(remainingDistance, remainingTime.getSeconds(), arrivalTime);
+
+        assertThat(travelEstimate.getRemainingDistance()).isEqualTo(remainingDistance);
+        assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(remainingTime.getSeconds());
+        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);
+        Duration remainingTime = Duration.ofHours(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, remainingTime.getSeconds(),
+                            arrivalTime)
+                            .setRemainingTimeColor(carColor)
+                            .build();
+
+            assertThat(travelEstimate.getRemainingDistance()).isEqualTo(remainingDistance);
+            assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(
+                    remainingTime.getSeconds());
+            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);
+        Duration remainingTime = Duration.ofHours(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, remainingTime.getSeconds(),
+                            arrivalTime)
+                            .setRemainingDistanceColor(carColor)
+                            .build();
+
+            assertThat(travelEstimate.getRemainingDistance()).isEqualTo(remainingDistance);
+            assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(
+                    remainingTime.getSeconds());
+            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);
+        Duration remainingTime = Duration.ofHours(10);
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        TravelEstimate.builder(remainingDistance, remainingTime.getSeconds(),
+                                arrivalTime)
+                                .setRemainingTimeColor(CarColor.createCustom(1, 2)));
+    }
+
+    @Test
+    public void equals() {
+        TravelEstimate travelEstimate = TravelEstimate.create(mRemainingDistance,
+                mRemainingTime.getSeconds(), mArrivalTime);
+
+        assertThat(travelEstimate)
+                .isEqualTo(
+                        TravelEstimate.create(mRemainingDistance, mRemainingTime.getSeconds(),
+                                mArrivalTime));
+    }
+
+    @Test
+    public void notEquals_differentRemainingDistance() {
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(mRemainingDistance, mRemainingTime.getSeconds(),
+                        mArrivalTime);
+
+        assertThat(travelEstimate)
+                .isNotEqualTo(
+                        TravelEstimate.create(
+                                Distance.create(/* displayDistance= */ 200, Distance.UNIT_METERS),
+                                mRemainingTime.getSeconds(),
+                                mArrivalTime));
+    }
+
+    @Test
+    public void notEquals_differentRemainingTime() {
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(mRemainingDistance, mRemainingTime.getSeconds(),
+                        mArrivalTime);
+
+        assertThat(travelEstimate)
+                .isNotEqualTo(
+                        TravelEstimate.create(mRemainingDistance, mRemainingTime.getSeconds() + 1,
+                                mArrivalTime));
+    }
+
+    @Test
+    public void notEquals_differentArrivalTime() {
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(mRemainingDistance, mRemainingTime.getSeconds(),
+                        mArrivalTime);
+
+        assertThat(travelEstimate)
+                .isNotEqualTo(
+                        TravelEstimate.create(
+                                mRemainingDistance,
+                                mRemainingTime.getSeconds(),
+                                createDateTimeWithZone("2020-04-14T15:57:01", "US/Pacific")));
+    }
+
+    @Test
+    public void notEquals_differentRemainingTimeColor() {
+        TravelEstimate travelEstimate =
+                TravelEstimate.builder(mRemainingDistance, mRemainingTime.getSeconds(),
+                        mArrivalTime)
+                        .setRemainingTimeColor(CarColor.YELLOW)
+                        .build();
+
+        assertThat(travelEstimate)
+                .isNotEqualTo(
+                        TravelEstimate.builder(mRemainingDistance, mRemainingTime.getSeconds(),
+                                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..761fa5b
--- /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.time.Duration;
+
+/** 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),
+                    Duration.ofHours(1).getSeconds(),
+                    createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific"));
+    private final TravelEstimate mDestinationTravelEstimate =
+            TravelEstimate.create(
+                    Distance.create(/* displayDistance= */ 100, Distance.UNIT_KILOMETERS),
+                    Duration.ofHours(1).getSeconds(),
+                    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/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/car/app/app/src/main/aidl/androidx/car/app/model/IOnClickListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/model/IOnClickListener.aidl
new file mode 100644
index 0000000..7308d03
--- /dev/null
+++ b/car/app/app/src/main/aidl/androidx/car/app/model/IOnClickListener.aidl
@@ -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.
+ */
+
+package androidx.car.app.model;
+
+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/car/app/app/src/main/java/androidx/car/app/model/Item.java b/car/app/app/src/main/java/androidx/car/app/model/Item.java
new file mode 100644
index 0000000..6e50daf
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/Item.java
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+/** 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/car/app/app/src/main/java/androidx/car/app/model/OnClickListener.java b/car/app/app/src/main/java/androidx/car/app/model/OnClickListener.java
new file mode 100644
index 0000000..f36ffca
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnClickListener.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+/** 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/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/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/main/java/androidx/navigation/NavController.java b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.java
index 9031ac4..d3a3b7d 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);
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 {